// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package conditions

import (
	"errors"

	"github.com/elastic/beats/v7/libbeat/common/match"
	"github.com/elastic/beats/v7/libbeat/logp"
)

const logName = "conditions"

// Config represents a configuration for a condition, as you would find it in the config files.
type Config struct {
	Equals    *Fields                `config:"equals"`
	Contains  *Fields                `config:"contains"`
	Regexp    *Fields                `config:"regexp"`
	Range     *Fields                `config:"range"`
	HasFields []string               `config:"has_fields"`
	Network   map[string]interface{} `config:"network"`
	OR        []Config               `config:"or"`
	AND       []Config               `config:"and"`
	NOT       *Config                `config:"not"`
}

// Condition is the interface for all defined conditions
type Condition interface {
	Check(event ValuesMap) bool
	String() string
}

// ValuesMap provides a common interface to read matchers for condition checking
type ValuesMap interface {
	// GetValue returns the given field from the map
	GetValue(string) (interface{}, error)
}

// NewCondition takes a Config and turns it into a real Condition
func NewCondition(config *Config) (Condition, error) {

	if config == nil {
		// empty condition
		return nil, errors.New("missing condition config")
	}

	var condition Condition
	var err error
	switch {
	case config.Equals != nil:
		condition, err = NewEqualsCondition(config.Equals.fields)
	case config.Contains != nil:
		condition, err = NewMatcherCondition("contains", config.Contains.fields, match.CompileString)
	case config.Regexp != nil:
		condition, err = NewMatcherCondition("regexp", config.Regexp.fields, match.Compile)
	case config.Range != nil:
		condition, err = NewRangeCondition(config.Range.fields)
	case config.HasFields != nil:
		condition = NewHasFieldsCondition(config.HasFields)
	case config.Network != nil && len(config.Network) > 0:
		condition, err = NewNetworkCondition(config.Network)
	case len(config.OR) > 0:
		var conditionsList []Condition
		conditionsList, err = NewConditionList(config.OR)
		condition = NewOrCondition(conditionsList)
	case len(config.AND) > 0:
		var conditionsList []Condition
		conditionsList, err = NewConditionList(config.AND)
		condition = NewAndCondition(conditionsList)
	case config.NOT != nil:
		var inner Condition
		inner, err = NewCondition(config.NOT)
		if err == nil {
			condition, err = NewNotCondition(inner)
		}
	default:
		err = errors.New("missing or invalid condition")
	}
	if err != nil {
		return nil, err
	}

	logp.L().Named(logName).Debugf("New condition %v", condition)
	return condition, nil
}

// NewConditionList takes a slice of Config objects and turns them into real Condition objects.
func NewConditionList(config []Config) ([]Condition, error) {
	out := make([]Condition, len(config))
	for i, condConfig := range config {
		cond, err := NewCondition(&condConfig)
		if err != nil {
			return nil, err
		}

		out[i] = cond
	}
	return out, nil
}
