// Package query ... implements functions to build elasticsearch query
// Usage example:
//
//  qb := NewQueryBuilder()
//	qb.AddRange("timestamp", "2019-04-21", "now").
//		AddEqual("Level", "error").
//		AddEqual("Source", "po.go").
//		AddEqual("Line", 129)
package query

import (
	"encoding/json"
	"reflect"

	"github.com/pingcap/log"
	"github.com/wushilin/stream"
	"go.uber.org/zap"
)

// It defines the elasticsearch query builder interface, customer can implements
// theirself elasticsearch build query by this interface
// Currently, just implements EquivalentQuery builder and RangeQuery builder
type IQuery interface {
	buildStatement() func(statement *esStatement)
	getTerms() map[string]interface{}
}

var registerQueries map[QueryType]func() *IQuery

type querybuilder struct {
	registerQueries map[QueryType]*IQuery
	from            int
	size            int
}

type QueryType int

const (
	EquivalentQueryType = iota
	RangeQueryType
	SortType
	INTYPE
)

//EquivalentQuery Builder is used to builder equivalent elasticsearch dsl
type EquivalentQuery struct {
	terms map[string]interface{}
}

func (q *EquivalentQuery) getTerms() map[string]interface{} {
	if q.terms == nil {
		q.terms = make(map[string]interface{})
	}

	return q.terms
}

func (q *EquivalentQuery) buildStatement() func(statement *esStatement) {
	return func(s *esStatement) {
		if isEmptyMap(q.terms) {
			return
		}

		stream.FromMapEntries(q.terms).
			Map(func(entry interface{}) interface{} {
				e := entry.(stream.MapEntry)
				return termEqual{
					Term: map[string]interface{}{
						e.Key.(reflect.Value).String(): e.Value,
					},
				}
			}).Each(func(te interface{}) {
			s.Query.Bool.Filter = append(s.Query.Bool.Filter, te)
		})

	}
}

type InQuery struct {
	terms map[string]interface{}
}

func (q *InQuery) getTerms() map[string]interface{} {
	if q.terms == nil {
		q.terms = make(map[string]interface{})
	}

	return q.terms
}

func (q *InQuery) buildStatement() func(statement *esStatement) {
	return func(s *esStatement) {
		if isEmptyMap(q.terms) {
			return
		}

		stream.FromMapEntries(q.terms).Map(func(entry interface{}) termsIn {
			e := entry.(stream.MapEntry)
			return termsIn{
				Terms: map[string]interface{}{
					e.Key.(reflect.Value).String(): e.Value,
				},
			}
		}).Each(func(t termsIn) {
			s.Query.Bool.Filter = append(s.Query.Bool.Filter, t)
		})

	}
}

// RangeQuery is used to buidler range elasticsearch dsl
type RangeQuery struct {
	terms map[string]interface{}
}

type rangeTerm struct {
	Min interface{} `json:"gte,omitempty"`
	Max interface{} `json:"lte,omitempty"`
}

func (q *RangeQuery) getTerms() map[string]interface{} {
	if q.terms == nil {
		q.terms = make(map[string]interface{})
	}

	return q.terms
}

func (q *RangeQuery) buildStatement() func(statement *esStatement) {

	return func(s *esStatement) {
		if isEmptyMap(q.terms) {
			return
		}

		stream.FromMapEntries(q.terms).Map(func(entry interface{}) interface{} {
			e := entry.(stream.MapEntry)
			return termRange{
				Range: map[string]rangeTerm{
					e.Key.(reflect.Value).String(): e.Value.(rangeTerm),
				},
			}
		}).Each(func(tr interface{}) {
			s.Query.Bool.Filter = append(s.Query.Bool.Filter, tr)
		})
	}
}

// Sort is used to buidler sort elasticsearch dsl

const (
	DESC string = "desc"
	ASC  string = "asc"
)

type sortStament struct {
	Sort map[string]interface{} `json:"sort,omitempty"`
}

func (q *sortStament) getTerms() map[string]interface{} {
	if q.Sort == nil {
		q.Sort = make(map[string]interface{})
	}

	return q.Sort
}

func (q *sortStament) buildStatement() func(statement *esStatement) {
	return func(s *esStatement) {
		if isEmptyMap(q.Sort) {
			return
		}

		for key, value := range q.Sort {
			s.Sort = map[string]string{
				key: getSortType(value.(bool)),
			}
		}
	}
}

func getSortType(desc bool) string {
	if desc {
		return DESC
	}

	return ASC
}

func isEmptyMap(m map[string]interface{}) bool {
	if m == nil || len(m) == 0 {
		return true
	}

	return false
}

//////////////////////////////////////////////////
// public methods
//////////////////////////////////////////////////

func NewQueryBuilder() *querybuilder {
	q := &querybuilder{
		registerQueries: make(map[QueryType]*IQuery),
	}

	eq := IQuery(&EquivalentQuery{
		terms: make(map[string]interface{}),
	})
	q.Register(EquivalentQueryType, &eq)

	rq := IQuery(&RangeQuery{
		terms: make(map[string]interface{}),
	})
	q.Register(RangeQueryType, &rq)

	ss := IQuery(&sortStament{
		Sort: make(map[string]interface{}),
	})
	q.Register(SortType, &ss)

	ti := IQuery(&InQuery{
		terms: make(map[string]interface{}),
	})
	q.Register(INTYPE, &ti)

	return q
}

func (q querybuilder) Register(qt QueryType, iq *IQuery) {
	q.registerQueries[qt] = iq
}

// AddEqual - It will build the term search definition using the Query DSL by the parameters
// Usage example:
// qb.AddEqual("Line", ", "129").
//   AddEqual("Level", "error")
func (q *querybuilder) AddEqual(name string, value interface{}) *querybuilder {
	if isZeroValue(value) {
		return q
	}

	(*q.registerQueries[EquivalentQueryType]).getTerms()[name] = value
	return q
}

// AddRange - It will build the range search definition using the Query DSL by the parameters
// Usage example:
// qb.AddRange("timestamp", "2019-04-21", ""2019-04-23")
//   .AddRange("Level", "debug", "error")
func (q *querybuilder) AddRange(name string, minValue interface{}, maxValue interface{}) *querybuilder {
	if isZeroValue(minValue) || isZeroValue(maxValue) {
		return q
	}

	(*q.registerQueries[RangeQueryType]).getTerms()[name] = rangeTerm{
		Min: minValue,
		Max: maxValue,
	}

	return q
}

func (q *querybuilder) AddIn(name string, values []interface{}) *querybuilder {
	if len(values) == 0 {
		return q
	}

	(*q.registerQueries[INTYPE]).getTerms()[name] = values
	return q
}

func (q *querybuilder) Sort(sortName string, desc bool) *querybuilder {
	if isZeroValue(sortName) {
		return q
	}

	(*q.registerQueries[SortType]).getTerms()[sortName] = desc

	return q
}

func (q *querybuilder) From(from int) *querybuilder {
	if from < 0 {
		from = 0
	}

	q.from = from
	return q
}

func (q *querybuilder) Size(size int) *querybuilder {
	if size <= 0 {
		size = 10
	}

	q.size = size
	return q
}

// BuildQuery - return search definition using the Query DSL
func (q *querybuilder) BuildQuery() string {
	statement := &esStatement{
		Query: &queryStatement{
			Bool: &boolStatement{
				Filter: make([]interface{}, 0),
			},
		},
		From: q.from,
		Size: q.size,
	}

	for _, query := range q.registerQueries {
		(*query).buildStatement()(statement)
	}

	queryStatement, err := json.Marshal(statement)
	if err != nil {
		log.Error("failed to build elasticsearch query.", zap.Error(err))
		return ""
	}
	return string(queryStatement)
}

func (q *querybuilder) GetQuery() string {
	statement := &esStatement{
		Query: &queryStatement{
			Bool: &boolStatement{
				Filter: make([]interface{}, 0),
			},
		},
	}

	for _, query := range q.registerQueries {
		(*query).buildStatement()(statement)
	}

	queryStatement, err := json.Marshal(statement.Query)
	if err != nil {
		log.Error("failed to build elasticsearch query.", zap.Error(err))
		return ""
	}
	return string(queryStatement)
}

type esStatement struct {
	Query *queryStatement   `json:"query,omitempty"`
	From  int               `json:"from"`
	Size  int               `json:"size"`
	Sort  map[string]string `json:"sort,omitempty"`
}

type queryStatement struct {
	Bool *boolStatement `json:"bool,omitempty"`
}

type boolStatement struct {
	Filter []interface{} `json:"filter,omitempty"`
}

type termEqual struct {
	Term map[string]interface{} `json:"term,omitempty"`
}

type termsIn struct {
	Terms map[string]interface{} `json:"terms,omitempty"`
}

type termRange struct {
	Range map[string]rangeTerm `json:"range,omitempty"`
}

func isZeroValue(v interface{}) bool {
	return v == reflect.Zero(reflect.TypeOf(v)).Interface()
}
