package task

import (
	"fmt"
	"regexp"
	"sort"

	"github.com/hashicorp/go-multierror"
	"github.com/scylladb/go-set/strset"

	"github.com/anchore/syft/syft/cataloging/pkgcataloging"
)

var expressionNodePattern = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-+]*)+$`)

const (
	SetOperation       Operation = "set"
	AddOperation       Operation = "add"
	SubSelectOperation Operation = "sub-select"
	RemoveOperation    Operation = "remove"
)

var (
	ErrEmptyToken       = fmt.Errorf("no value given")
	ErrInvalidToken     = fmt.Errorf("invalid token given: only alphanumeric characters and hyphens are allowed")
	ErrInvalidOperator  = fmt.Errorf("invalid operator given")
	ErrUnknownNameOrTag = fmt.Errorf("unknown name or tag given")
	ErrTagsNotAllowed   = fmt.Errorf("tags are not allowed with this operation (must use exact names)")
	ErrNamesNotAllowed  = fmt.Errorf("names are not allowed with this operation (must use tags)")
	ErrAllNotAllowed    = fmt.Errorf("cannot use the 'all' operand in this context")
)

// ErrInvalidExpression represents an expression that cannot be parsed or can be parsed but is logically invalid.
type ErrInvalidExpression struct {
	Expression string
	Operation  Operation
	Err        error
}

func (e ErrInvalidExpression) Error() string {
	return fmt.Sprintf("invalid expression: %q: %s", e.Expression, e.Err.Error())
}

func newErrInvalidExpression(exp string, op Operation, err error) ErrInvalidExpression {
	return ErrInvalidExpression{
		Expression: exp,
		Operation:  op,
		Err:        err,
	}
}

// Expression represents a single operation-operand pair with (all validation errors).
// E.g. "+foo", "-bar", or "something" are all expressions. Some validations are relevant to not only the
// syntax (operation and operator) but other are sensitive to the context of the operand (e.g. if a given operand
// is a tag or a name, validated against the operation).
type Expression struct {
	Operation Operation
	Operand   string
	Errors    []error
}

// Operation represents the type of operation to perform on the operand (set, add, remove, sub-select).
type Operation string

// Expressions represents a list of expressions.
type Expressions []Expression

// expressionContext represents all information needed to validate an expression (e.g. the set of all tasks and their tags).
type expressionContext struct {
	Names *strset.Set
	Tags  *strset.Set
}

func newExpressionContext(ts []Task) *expressionContext {
	ec := &expressionContext{
		Names: strset.New(tasks(ts).Names()...),
		Tags:  strset.New(tasks(ts).Tags()...),
	}

	ec.Tags.Add("all")

	return ec
}

// newExpression creates a new validated Expression object relative to the task names and tags.
func (ec expressionContext) newExpression(exp string, operation Operation, token string) Expression {
	if token == "" {
		return Expression{
			Operation: operation,
			Operand:   token,
			Errors:    []error{newErrInvalidExpression(exp, operation, ErrEmptyToken)},
		}
	}

	if !isValidNode(token) {
		return Expression{
			Operation: operation,
			Operand:   token,
			Errors:    []error{newErrInvalidExpression(exp, operation, ErrInvalidToken)},
		}
	}

	var err error
	switch operation {
	case SetOperation, RemoveOperation:
		// names and tags allowed
		if !ec.Tags.Has(token) && !ec.Names.Has(token) {
			err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
		}
	case AddOperation:
		// only names are allowed
		if !ec.Names.Has(token) {
			if ec.Tags.Has(token) {
				err = newErrInvalidExpression(exp, operation, ErrTagsNotAllowed)
			} else {
				err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
			}
		}
	case SubSelectOperation:
		if token == "all" {
			// special case: we cannot sub-select all (this is most likely a misconfiguration and the user intended to use the set operation)
			err = newErrInvalidExpression(exp, operation, ErrAllNotAllowed)
		} else if !ec.Tags.Has(token) {
			// only tags are allowed...
			if ec.Names.Has(token) {
				err = newErrInvalidExpression(exp, operation, ErrNamesNotAllowed)
			} else {
				err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
			}
		}
	}

	var errs []error
	if err != nil {
		errs = append(errs, err)
	}

	return Expression{
		Operation: operation,
		Operand:   token,
		Errors:    errs,
	}
}

func newExpressionsFromSelectionRequest(nc *expressionContext, selectionRequest pkgcataloging.SelectionRequest) Expressions {
	var all Expressions

	for _, exp := range selectionRequest.DefaultNamesOrTags {
		all = append(all, nc.newExpression(exp, SetOperation, exp))
	}

	for _, exp := range selectionRequest.SubSelectTags {
		all = append(all, nc.newExpression(exp, SubSelectOperation, exp))
	}

	for _, exp := range selectionRequest.AddNames {
		all = append(all, nc.newExpression(exp, AddOperation, exp))
	}

	for _, exp := range selectionRequest.RemoveNamesOrTags {
		all = append(all, nc.newExpression(exp, RemoveOperation, exp))
	}

	sort.Sort(all)
	return all
}

func isValidNode(s string) bool {
	return expressionNodePattern.Match([]byte(s))
}

func (e Expressions) Clone() Expressions {
	clone := make(Expressions, len(e))
	copy(clone, e)
	return clone
}

func (e Expression) String() string {
	var op string
	switch e.Operation {
	case AddOperation:
		op = "+"
	case RemoveOperation:
		op = "-"
	case SubSelectOperation:
		op = ""
	case SetOperation:
		op = ""
	default:
		op = "?"
	}
	return op + e.Operand
}

func (e Expressions) Len() int {
	return len(e)
}

func (e Expressions) Swap(i, j int) {
	e[i], e[j] = e[j], e[i]
}

// order of operations
var orderOfOps = map[Operation]int{
	SetOperation:       1,
	SubSelectOperation: 2,
	RemoveOperation:    3,
	AddOperation:       4,
}

func (e Expressions) Less(i, j int) bool {
	ooi := orderOfOps[e[i].Operation]
	ooj := orderOfOps[e[j].Operation]

	if ooi != ooj {
		return ooi < ooj
	}

	return i < j
}

func (e Expressions) Errors() (errs []error) {
	for _, n := range e {
		if len(n.Errors) > 0 {
			errs = append(errs, n.Errors...)
		}
	}
	return errs
}

func (e Expressions) Validate() error {
	errs := e.Errors()
	if len(errs) == 0 {
		return nil
	}
	var err error
	return multierror.Append(err, e.Errors()...)
}
