package es

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"

	"github.com/opensearch-project/opensearch-go"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/stream"
)

type ESClient struct {
	cli *opensearch.Client
}

type Response struct {
	AggResponse AggResponse

	Results []map[string]any
}

type AggResponse struct {
	Buckets map[string]int64
}

func NewESClient(cfg config.LogConfig) *ESClient {
	cli, err := AwsEsClient(cfg.Endpoint, cfg.User, cfg.Password)
	if err != nil {
		panic(err)
	}

	return &ESClient{
		cli: cli,
	}
}

func (q *ESClient) Do(ctx context.Context, builder *queryBuilder) (*Response, error) {
	query, err := builder.Build()
	if err != nil {
		return nil, err
	}

	log.Info("query es statement", zap.String("index", builder.index), zap.Any("query", query.String()))
	res, err := q.cli.Search(q.cli.Search.WithContext(ctx),
		q.cli.Search.WithIndex(builder.index),
		q.cli.Search.WithBody(query),
		q.cli.Search.WithSize(builder.size),
		q.cli.Search.WithTrackTotalHits(true),
		q.cli.Search.WithPretty())
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	if res.IsError() {
		var e map[string]any
		if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
			log.Error("Error parsing the response body", zap.Error(err))
			return nil, err
		} else {
			return nil, fmt.Errorf("query failed. query=%s, status=%s, type=%s, reason=%s", query, res.Status(), e["error"].(map[string]any)["type"], e["error"].(map[string]any)["reason"])
		}
	}

	var r map[string]any
	if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
		log.Error("Error parsing the response body", zap.Error(err))
		return nil, err
	}

	if agg, ok := r["aggregations"]; ok {
		return buildAggResponse(agg), nil
	}

	if r["hits"].(map[string]any)["total"].(map[string]any)["value"].(float64) == 0 {
		return nil, nil
	}

	results := make([]map[string]any, 0)
	for _, hit := range r["hits"].(map[string]any)["hits"].([]any) {
		hs := hit.(map[string]any)["_source"].(map[string]any)
		hs["id"] = hit.(map[string]any)["_id"]
		if t, ok := hs["timestamp"]; ok {
			hs["timestamp"] = int64(t.(float64)) / 1000
		}
		results = append(results, hs)
	}

	return &Response{
		Results: results,
	}, nil
}

func buildAggResponse(aggs any) *Response {
	agg, ok := (aggs.(map[string]any))["aggs"]
	if !ok {
		log.Warn("empty aggs")
		return nil
	}

	buckets, ok := (agg.(map[string]any))["buckets"]
	if !ok {
		log.Warn("empty buckets")
		return nil
	}

	b, ok := buckets.([]any)
	if !ok {
		log.Warn("convert buckets to interface list failed")
		return nil
	}

	m := make(map[string]int64)
	stream.OfSlice(b).Map(func(i any) map[string]any {
		d, ok := i.(map[string]any)
		if !ok {
			log.Warn("convert bucket to map failed")
			return nil
		}

		return d
	}).Filter(func(i map[string]any) bool {
		return i != nil
	}).Each(func(i map[string]any) {
		if k, ok := i["key"]; ok {
			m[k.(string)] = int64(i["doc_count"].(float64))
		}
	})

	return &Response{
		AggResponse: AggResponse{
			Buckets: m,
		},
	}
}

type query interface {
	query() *queryEntry
}

type queryBuilder struct {
	index string
	ands  []*queryEntry

	from int
	size int
	sort map[string]SortType
}

type QueryBuildOption struct {
	From int
	Size int
	Sort map[string]SortType
}

func NewQueryBuilder(index string, bq *baseQuery, aq *aggQuery, opt *QueryBuildOption) *queryBuilder {
	ands := make([]*queryEntry, 0)
	if bq != nil {
		ands = append(ands, bq.query())
	}

	if aq != nil {
		ands = append(ands, aq.query())
	}

	qb := &queryBuilder{
		index: index,
		ands:  ands,
	}

	if opt != nil {
		qb.from = opt.From
		qb.size = opt.Size
		qb.sort = opt.Sort
	}

	return qb
}

//func (q *queryBuilder) And(query query) *queryBuilder{
//	st := query.query()
//	if st == nil {
//		return q
//	}
//	q.ands = append(q.ands, st)
//
//	return q
//}

func (q *queryBuilder) Build() (*bytes.Buffer, error) {
	r := make(map[string]any)
	if q.from != 0 {
		r["from"] = q.from
	}

	if q.size != 0 {
		r["size"] = q.size
	}

	if len(q.sort) > 0 {
		m := make([]map[string]map[string]string, 0)
		for k, v := range q.sort {
			m = append(m, map[string]map[string]string{
				k: {
					"order": string(v),
				},
			})
		}
		r["sort"] = m
	}

	stream.OfSlice(q.ands).Filter(func(qe *queryEntry) bool {
		return qe != nil
	}).Each(func(qe *queryEntry) {
		r[qe.key] = qe.value
	})

	var buf bytes.Buffer
	err := json.NewEncoder(&buf).Encode(r)
	if err != nil {
		log.Error("build query failed", zap.Error(err))
		return nil, err
	}

	return &buf, nil
}

// only support filter statemnte
type baseQuery struct {
	ands    []*queryEntry
	shoulds []*queryEntry
}

type SortType string

var (
	Desc SortType = "desc"
	Asc  SortType = "asc"
)

func NewBaseQuery() *baseQuery {
	return &baseQuery{
		ands:    make([]*queryEntry, 0),
		shoulds: make([]*queryEntry, 0),
	}
}

func (q *baseQuery) And(query query) *baseQuery {
	q.ands = append(q.ands, query.query())

	return q
}

func (q *baseQuery) Should(query query) *baseQuery {
	q.shoulds = append(q.shoulds, query.query())

	return q
}

type boolQueryStatement struct {
	BoolStatement boolStatement `json:"bool"`
}

type boolStatement struct {
	filterStatement
	shouldStatement

	Minshouldmatch int `json:"minimum_should_match,omitempty"`
}

type shouldStatement struct {
	Should []map[string]any `json:"should,omitempty"`
}

type filterStatement struct {
	Filters []map[string]any `json:"filter,omitempty"`
}

func (q *baseQuery) query() *queryEntry {
	filters := make([]map[string]any, 0)

	stream.OfSlice(q.ands).Each(func(q *queryEntry) {
		filters = append(filters, map[string]any{
			q.key: q.value,
		})
	})

	shoulds := make([]map[string]any, 0)
	stream.OfSlice(q.shoulds).Each(func(q *queryEntry) {
		shoulds = append(shoulds, map[string]any{
			q.key: q.value,
		})
	})

	boolStat := boolStatement{
		filterStatement: filterStatement{
			Filters: filters,
		},
		shouldStatement: shouldStatement{
			Should: shoulds,
		},
	}

	if len(shoulds) > 0 {
		boolStat.Minshouldmatch = 1
	}

	return &queryEntry{
		key: "query",
		value: boolQueryStatement{
			BoolStatement: boolStat,
		},
	}
}

type wildCardQuery struct {
	key   string
	value any
}

func NewWildCardQuery(key string, value any) *wildCardQuery {
	return &wildCardQuery{
		key:   key,
		value: value,
	}
}

func (t *wildCardQuery) query() *queryEntry {
	return &queryEntry{
		key: "wildcard",
		value: map[string]any{
			t.key: t.value,
		},
	}
}

type termQuery struct {
	key   string
	value any
}

func NewTermQuery(key string, value any) *termQuery {
	return &termQuery{
		key:   key,
		value: value,
	}
}

func (t *termQuery) query() *queryEntry {
	return &queryEntry{
		key: "term",
		value: map[string]any{
			t.key: t.value,
		},
	}
}

type rangeQuery struct {
	key string
	gte int64
	lte int64
}

func NewRangeQuery(key string, gte, lte int64) *rangeQuery {
	return &rangeQuery{
		key: key,
		gte: gte,
		lte: lte,
	}
}

func (q *rangeQuery) query() *queryEntry {
	return &queryEntry{
		key: "range",
		value: map[string]map[string]int64{
			q.key: {
				"gte": q.gte,
				"lte": q.lte,
			},
		},
	}
}

type aggQuery struct {
	field string
}

func NewAggQuery(field string) *aggQuery {
	return &aggQuery{
		field: field,
	}
}

type aggsStatement struct {
	AggStatement aggStatement `json:"aggs"`
}

type aggStatement struct {
	Terms Terms `json:"terms"`
}

type Terms struct {
	Field string `json:"field"`
}

func (a *aggQuery) query() *queryEntry {
	ag := aggsStatement{
		AggStatement: aggStatement{
			Terms: Terms{
				Field: a.field,
			},
		},
	}

	return &queryEntry{
		key:   "aggs",
		value: ag,
	}
}

type queryEntry struct {
	key   string
	value any
}
