// 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 outil

import (
	"fmt"

	"github.com/elastic/beats/libbeat/beat"
	"github.com/elastic/beats/libbeat/common"
	"github.com/elastic/beats/libbeat/common/fmtstr"
	"github.com/elastic/beats/libbeat/conditions"
)

type Selector struct {
	sel SelectorExpr
}

type Settings struct {
	// single selector key and default option keyword
	Key string

	// multi-selector key in config
	MultiKey string

	// if enabled a selector `key` in config will be generated, if `key` is present
	EnableSingleOnly bool

	// Fail building selector if `key` and `multiKey` are missing
	FailEmpty bool
}

type SelectorExpr interface {
	sel(evt *beat.Event) (string, error)
}

type emptySelector struct{}

type listSelector struct {
	selectors []SelectorExpr
}

type condSelector struct {
	s    SelectorExpr
	cond conditions.Condition
}

type constSelector struct {
	s string
}

type fmtSelector struct {
	f         fmtstr.EventFormatString
	otherwise string
}

type mapSelector struct {
	from      SelectorExpr
	otherwise string
	to        map[string]string
}

var nilSelector SelectorExpr = &emptySelector{}

func MakeSelector(es ...SelectorExpr) Selector {
	switch len(es) {
	case 0:
		return Selector{nilSelector}
	case 1:
		return Selector{es[0]}
	default:
		return Selector{ConcatSelectorExpr(es...)}
	}
}

// Select runs configured selector against the current event.
// If no matching selector is found, an empty string is returned.
// It's up to the caller to decide if an empty string is an error
// or an expected result.
func (s Selector) Select(evt *beat.Event) (string, error) {
	return s.sel.sel(evt)
}

func (s Selector) IsEmpty() bool {
	return s.sel == nilSelector || s.sel == nil
}

func (s Selector) IsConst() bool {
	if s.sel == nilSelector {
		return true
	}

	_, ok := s.sel.(*constSelector)
	return ok
}

func BuildSelectorFromConfig(
	cfg *common.Config,
	settings Settings,
) (Selector, error) {
	var sel []SelectorExpr

	key := settings.Key
	multiKey := settings.MultiKey
	found := false

	if cfg.HasField(multiKey) {
		found = true
		sub, err := cfg.Child(multiKey, -1)
		if err != nil {
			return Selector{}, err
		}

		var table []*common.Config
		if err := sub.Unpack(&table); err != nil {
			return Selector{}, err
		}

		for _, config := range table {
			action, err := buildSingle(config, key)
			if err != nil {
				return Selector{}, err
			}

			if action != nilSelector {
				sel = append(sel, action)
			}
		}
	}

	if settings.EnableSingleOnly && cfg.HasField(key) {
		found = true

		// expect event-format-string
		str, err := cfg.String(key, -1)
		if err != nil {
			return Selector{}, err
		}

		fmtstr, err := fmtstr.CompileEvent(str)
		if err != nil {
			return Selector{}, fmt.Errorf("%v in %v", err, cfg.PathOf(key))
		}

		if fmtstr.IsConst() {
			str, err := fmtstr.Run(nil)
			if err != nil {
				return Selector{}, err
			}

			if str != "" {
				sel = append(sel, ConstSelectorExpr(str))
			}
		} else {
			sel = append(sel, FmtSelectorExpr(fmtstr, ""))
		}
	}

	if settings.FailEmpty && !found {
		if settings.EnableSingleOnly {
			return Selector{}, fmt.Errorf("missing required '%v' or '%v' in %v",
				key, multiKey, cfg.Path())
		}

		return Selector{}, fmt.Errorf("missing required '%v' in %v",
			multiKey, cfg.Path())
	}

	return MakeSelector(sel...), nil
}

func EmptySelectorExpr() SelectorExpr {
	return nilSelector
}

func ConstSelectorExpr(s string) SelectorExpr {
	return &constSelector{s}
}

func FmtSelectorExpr(fmt *fmtstr.EventFormatString, fallback string) SelectorExpr {
	return &fmtSelector{*fmt, fallback}
}

func ConcatSelectorExpr(s ...SelectorExpr) SelectorExpr {
	return &listSelector{s}
}

func ConditionalSelectorExpr(
	s SelectorExpr,
	cond conditions.Condition,
) SelectorExpr {
	return &condSelector{s, cond}
}

func LookupSelectorExpr(
	s SelectorExpr,
	table map[string]string,
	fallback string,
) SelectorExpr {
	return &mapSelector{s, fallback, table}
}

func buildSingle(cfg *common.Config, key string) (SelectorExpr, error) {
	// TODO: check for unknown fields

	// 1. extract required key-word handler
	if !cfg.HasField(key) {
		return nil, fmt.Errorf("missing %v", cfg.PathOf(key))
	}

	str, err := cfg.String(key, -1)
	if err != nil {
		return nil, err
	}

	evtfmt, err := fmtstr.CompileEvent(str)
	if err != nil {
		return nil, fmt.Errorf("%v in %v", err, cfg.PathOf(key))
	}

	// 2. extract optional `default` value
	var otherwise string
	if cfg.HasField("default") {
		tmp, err := cfg.String("default", -1)
		if err != nil {
			return nil, err
		}
		otherwise = tmp
	}

	// 3. extract optional `mapping`
	mapping := struct {
		Table map[string]string `config:"mappings"`
	}{nil}
	if cfg.HasField("mappings") {
		if err := cfg.Unpack(&mapping); err != nil {
			return nil, err
		}
	}

	// 4. extract conditional
	var cond conditions.Condition
	if cfg.HasField("when") {
		sub, err := cfg.Child("when", -1)
		if err != nil {
			return nil, err
		}

		condConfig := conditions.Config{}
		if err := sub.Unpack(&condConfig); err != nil {
			return nil, err
		}

		tmp, err := conditions.NewCondition(&condConfig)
		if err != nil {
			return nil, err
		}

		cond = tmp
	}

	// 5. build selector from available fields
	var sel SelectorExpr
	if len(mapping.Table) > 0 {
		if evtfmt.IsConst() {
			str, err := evtfmt.Run(nil)
			if err != nil {
				return nil, err
			}

			str = mapping.Table[str]
			if str == "" {
				str = otherwise
			}

			if str == "" {
				sel = nilSelector
			} else {
				sel = ConstSelectorExpr(str)
			}
		} else {
			sel = &mapSelector{
				from:      FmtSelectorExpr(evtfmt, ""),
				to:        mapping.Table,
				otherwise: otherwise,
			}
		}
	} else {
		if evtfmt.IsConst() {
			str, err := evtfmt.Run(nil)
			if err != nil {
				return nil, err
			}

			if str == "" {
				sel = nilSelector
			} else {
				sel = ConstSelectorExpr(str)
			}
		} else {
			sel = FmtSelectorExpr(evtfmt, otherwise)
		}
	}
	if cond != nil && sel != nilSelector {
		sel = ConditionalSelectorExpr(sel, cond)
	}

	return sel, nil
}

func (s *emptySelector) sel(evt *beat.Event) (string, error) {
	return "", nil
}

func (s *listSelector) sel(evt *beat.Event) (string, error) {
	for _, sub := range s.selectors {
		n, err := sub.sel(evt)
		if err != nil { // TODO: try
			return n, err
		}

		if n != "" {
			return n, nil
		}
	}

	return "", nil
}

func (s *condSelector) sel(evt *beat.Event) (string, error) {
	if !s.cond.Check(evt) {
		return "", nil
	}
	return s.s.sel(evt)
}

func (s *constSelector) sel(_ *beat.Event) (string, error) {
	return s.s, nil
}

func (s *fmtSelector) sel(evt *beat.Event) (string, error) {
	n, err := s.f.Run(evt)
	if err != nil {
		// err will be set if not all keys present in event ->
		// return empty selector result and ignore error
		return s.otherwise, nil
	}

	if n == "" {
		return s.otherwise, nil
	}
	return n, nil
}

func (s *mapSelector) sel(evt *beat.Event) (string, error) {
	n, err := s.from.sel(evt)
	if err != nil {
		if s.otherwise == "" {
			return "", err
		}
		return s.otherwise, nil
	}

	if n == "" {
		return s.otherwise, nil
	}

	n = s.to[n]
	if n == "" {
		return s.otherwise, nil
	}
	return n, nil
}
