package log

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"time"

	"github.com/onsi/gomega"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/cluster"
	"github.com/pingcap-inc/clinic/pkg/es"
)

const (
	timeout  = time.Second * 120
	interval = time.Second * 1
)

func QueryESEventually(opt IQueryOption, g *gomega.WithT, expectedCount int) []Record {
	var logs []Record
	g.Eventually(func() bool {
		records, err := ESQueryRecord(opt, expectedCount)
		if err != nil {
			log.Error("query data from es failed", zap.Error(err))
			return false
		}

		if len(records) == expectedCount {
			logs = records
			return true
		}

		return false
	}, timeout, interval).Should(gomega.BeTrue())

	return logs
}

func QueryESSlowQueryLogEventually(opt IQueryOption, g *gomega.WithT, expectedCount int) []SlowQueryRecord {
	var logs []SlowQueryRecord
	g.Eventually(func() bool {
		records, err := ESQuerySlowQueryRecord(opt, expectedCount)
		if err != nil {
			log.Error("query slow log from es failed", zap.Error(err))
			return false
		}

		if len(records) == expectedCount {
			logs = records
			return true
		}

		return false
	}, timeout, interval).Should(gomega.BeTrue())

	return logs
}

func TestClusterJSON(path string) (*cluster.ClusterInfo, error) {
	// var cj collector.ClusterJSON
	clusterFile, err := os.Open(path)
	if err != nil {
		return nil, err
	}

	data, err := io.ReadAll(clusterFile)
	if err != nil {
		return nil, err
	}
	return cluster.UnmarshalJSON(data)
}

type IQueryOption interface {
	BuildQuery() []byte
	GetIndex() string
}
type ESQueryOption struct {
	ClusterID  string
	Host       string
	Component  string
	StartTime  int
	EndTime    int
	Level      string
	InstanceID string
	OrgID      uint
}

func (opt *ESQueryOption) GetIndex() string {
	return es.GetLogIndexName(opt.OrgID)
}

func (opt *ESQueryOption) BuildQuery() []byte {
	query := `{
    "query": {
        "bool": {
            "filter": [
               {
                    "term": {
                        "cluster_id": "%s"
                    }
                },               
                {
                    "term": {
                        "host": "%s"
                    }
                },
                {
                    "term": {
                        "level": "%s"
                    }
                },
                {
                    "term": {
                        "component": "%s"
                    }
                },
                {
                    "term": {
                        "instanceid": "%s"
                    }
                },
                {
                    "range": {
                        "timestamp": {
                            "gte": "%d",
                            "lt": "%d"
                        }
                    }
                }
            ]
        }
    }
}`
	return []byte(fmt.Sprintf(query, opt.ClusterID, opt.Host, opt.Level, opt.Component, opt.InstanceID, opt.StartTime, opt.EndTime))
}

type ESQuerySlowQueryLogOption struct {
	ClusterID  string
	Host       string
	Component  string
	StartTime  int
	EndTime    int
	InstanceID string
	OrgID      uint
}

func (opt *ESQuerySlowQueryLogOption) GetIndex() string {
	return es.GetSlowLogIndexName(opt.OrgID)
}

func (opt *ESQuerySlowQueryLogOption) BuildQuery() []byte {
	query := `{
    "query": {
        "bool": {
            "filter": [
                {
                    "term": {
                        "host": "%s"
                    }
                },
                {
                    "term": {
                        "component": "%s"
                    }
                },
                {
                    "term": {
                        "instanceid": "%s"
                    }
                },
                {
                    "range": {
                        "timestamp": {
                            "gte": "%d",
                            "lt": "%d"
                        }
                    }
                }
            ]
        }
    },
   "sort" : [
      {"timestamp" : {"order" : "asc"}}
   ]
}`
	return []byte(fmt.Sprintf(query, opt.Host, opt.Component, opt.InstanceID, opt.StartTime, opt.EndTime))
}

func ESQueryRecord(opt IQueryOption, expectedCount int) ([]Record, error) {
	query := opt.BuildQuery()
	cli, err := es.AwsEsClient(fmt.Sprintf("http://127.0.0.1:%d", es.EsPort), "admin", "admin")
	if err != nil {
		return nil, err
	}

	var mjson map[string]any
	err = json.Unmarshal(query, &mjson)
	if err != nil {
		return nil, err
	}

	var buf bytes.Buffer
	err = json.NewEncoder(&buf).Encode(mjson)
	if err != nil {
		return nil, err
	}
	res, err := cli.Search(cli.Search.WithContext(context.Background()),
		cli.Search.WithIndex(opt.GetIndex()),
		cli.Search.WithBody(&buf),
		cli.Search.WithSize(expectedCount),
		cli.Search.WithTrackTotalHits(true),
		cli.Search.WithPretty())
	if err != nil {
		log.Error("query es field", zap.Error(err))
		return nil, errors.Errorf("query=%s, err=%v", string(query), err)
	}

	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 {
			log.Error("query es field", zap.Error(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"])))

			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
	}

	logs := make([]Record, 0)
	if r["hits"].(map[string]any)["total"].(map[string]any)["value"].(float64) == 0 {
		log.Error("query es return hints empty", zap.Any("hints", r["hits"]))
		return nil, nil
	}

	for _, hit := range r["hits"].(map[string]any)["hits"].([]any) {
		hs := hit.(map[string]any)["_source"].(map[string]any)
		logs = append(logs, Record{
			Timestamp: int64(hs["timestamp"].(float64)),
			Level:     hs["level"].(string),
			Message:   hs["message"].(string),
			Host:      hs["host"].(string),
			Component: hs["component"].(string),
		})
	}

	return logs, nil
}

func ESQuerySlowQueryRecord(opt IQueryOption, expectedCnt int) ([]SlowQueryRecord, error) {
	query := opt.BuildQuery()
	cli, err := es.AwsEsClient(fmt.Sprintf("http://127.0.0.1:%d", es.EsPort), "admin", "admin")
	if err != nil {
		return nil, err
	}
	if err != nil {
		return nil, err
	}

	var mjson map[string]any
	err = json.Unmarshal(query, &mjson)
	if err != nil {
		return nil, err
	}

	var buf bytes.Buffer
	err = json.NewEncoder(&buf).Encode(mjson)
	if err != nil {
		return nil, err
	}

	res, err := cli.Search(cli.Search.WithContext(context.Background()),
		cli.Search.WithIndex(opt.GetIndex()),
		cli.Search.WithBody(&buf),
		cli.Search.WithSize(expectedCnt),
		cli.Search.WithTrackTotalHits(true),
		cli.Search.WithPretty())
	if err != nil {
		return nil, errors.Errorf("query=%s, err=%v", string(query), 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"])
		}
	}
	type EsResp struct {
		Hits struct {
			Total struct {
				Value int64 `json:"value"`
			} `json:"total"`
			Hits []struct {
				Source SlowQueryRecord `json:"_source"`
			}
		} `json:"hits"`
	}
	r := EsResp{}
	if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
		log.Error("Error parsing the response body", zap.Error(err))
		return nil, err
	}

	logs := make([]SlowQueryRecord, 0)
	if r.Hits.Total.Value == 0 {
		return nil, nil
	}
	for _, hit := range r.Hits.Hits {
		logs = append(logs, hit.Source)
	}
	return logs, nil
}
