// Copyright 2021 Molecula Corp. All rights reserved.
package main

import (
	"context"
	"expvar"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	_ "net/http/pprof"
	"os"
	"sort"
	"strings"
	"time"

	pilosa "github.com/molecula/featurebase/v3"
	"github.com/molecula/featurebase/v3/encoding/proto"
	"golang.org/x/sync/errgroup"
)

var (
	requestCountVar          = expvar.NewInt("request_count")
	requestCurrentLatencyVar = expvar.NewFloat("request_current_latency") // seconds
	requestAvgLatencyVar     = expvar.NewFloat("request_avg_latency")     // seconds
	requestTotalLatencyVar   = expvar.NewFloat("request_total_latency")   // seconds
	requestPerSecVar         = expvar.NewFloat("request_per_sec")
)

func main() {
	if err := run(context.Background(), os.Args[1:]); err == flag.ErrHelp {
		os.Exit(1)
	} else if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run(ctx context.Context, args []string) (err error) {
	fs := flag.NewFlagSet("pilosa-bench", flag.ContinueOnError)
	hostport := fs.String("hostport", "localhost:10101", "")
	typ := fs.String("type", "row", "query type (row)")
	n := fs.Int("n", 1000, "number of queries")
	rate := fs.Int("rate", 1, "number of queries per second")
	verbose := fs.Bool("v", false, "verbose logging")
	from := fs.String("from", "", "from time for row-range queries (ISO 8601)")
	to := fs.String("to", "", "to time for row-range queries (ISO 8601)")
	if err := fs.Parse(args); err != nil {
		return err
	}

	// Parse from/to time.
	var opt queryOptions
	if *from != "" {
		if opt.from, err = time.Parse(time.RFC3339, *from); err != nil {
			return fmt.Errorf("cannot parse -from time")
		}
	}
	if *to != "" {
		if opt.to, err = time.Parse(time.RFC3339, *to); err != nil {
			return fmt.Errorf("cannot parse -to time")
		}
	}

	if (*typ == "row-range" || *typ == "topk") && (opt.from.IsZero() || opt.to.IsZero()) {
		return fmt.Errorf("-from and -to flags must be specified for topk & row-range queries")
	}

	// Clear time prefix on log.
	log.SetFlags(0)
	if !*verbose {
		log.SetOutput(ioutil.Discard)
	}

	// Setup PRNG to have consistent values for the same set of data.
	rand.Seed(0)

	// Setup connection to pilosa.
	client, err := pilosa.NewInternalClient(*hostport, http.DefaultClient, pilosa.WithSerializer(proto.Serializer{}))
	if err != nil {
		return err
	}

	// Set up HTTP endpoint to provide /debug endpoints.
	fmt.Println("Serving debug endpoint at http://localhost:7070/debug")
	go func() { _ = http.ListenAndServe(":7070", nil) }()

	// Run separate goroutine to calculate the current req/sec & latency.
	go monitor()

	// Load all id/keys for each field.
	log.Printf("loading field identifiers")
	fieldIDMap, err := loadFields(ctx, client)
	if err != nil {
		return fmt.Errorf("cannot load field identifiers: %w", err)
	} else if len(fieldIDMap) == 0 {
		return fmt.Errorf("no field identifiers available, please verify data exists")
	}

	// Generate list of sorted keys.
	fieldKeys := make([]fieldKey, 0, len(fieldIDMap))
	for k, f := range fieldIDMap {
		switch *typ {
		case "row-bsi":
			if f.info.Options.Type != "int" {
				continue
			}
		case "row-range", "topk":
			if f.info.Options.Type != "time" {
				continue
			}
		default:
			if f.info.Options.Type == "int" || f.info.Options.Type == "time" {
				continue
			}
		}

		fieldKeys = append(fieldKeys, k)
	}
	sort.Slice(fieldKeys, func(i, j int) bool {
		return compareFieldKeys(fieldKeys[i], fieldKeys[j]) == -1
	})

	// Ensure we have appropriate fields for our query type.
	if len(fieldKeys) == 0 {
		return fmt.Errorf("no available fields are appropriate for %q queries", *typ)
	}

	log.Printf("issuing %d queries at %d query/sec", *n, *rate)

	// Repeatedly issue queries based on available row data.
	ticker := time.NewTicker(time.Second / time.Duration(*rate))
	var g errgroup.Group
	for i := 0; i < *n; i++ {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-ticker.C:
		}

		key := fieldKeys[rand.Intn(len(fieldKeys))]
		q, err := generateQuery(*typ, key.index, key.field, fieldIDMap[key].info, fieldIDMap[key].identifiers, opt)
		if err != nil {
			return fmt.Errorf("cannot generate query: %w", err)
		}

		log.Printf("[query] %s", q)

		g.Go(func() error {
			t := time.Now()
			_, err = client.Query(ctx, key.index, &pilosa.QueryRequest{Index: key.index, Query: q})
			if err != nil {
				return err
			}
			elapsed := time.Since(t).Seconds()
			requestCountVar.Add(1)
			requestTotalLatencyVar.Add(elapsed)
			requestAvgLatencyVar.Set(requestTotalLatencyVar.Value() / float64(requestCountVar.Value()))
			return nil
		})
	}

	return g.Wait()
}

// monitor runs in a separate goroutine and updates metrics.
func monitor() {
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	var lastTime time.Time
	var lastN int64
	var lastLatency float64
	for range ticker.C {
		now, n := time.Now(), requestCountVar.Value()
		latency := requestTotalLatencyVar.Value()

		if !lastTime.IsZero() {
			elapsed := lastTime.Sub(now).Seconds()
			if n > 0 {
				requestCurrentLatencyVar.Set((lastLatency - latency) / float64(n))
			}
			requestPerSecVar.Set(float64(lastN-n) / elapsed)
		}
		lastTime, lastN, lastLatency = now, n, latency
	}
}

func generateQuery(typ, index, field string, info *pilosa.FieldInfo, identifiers *pilosa.RowIdentifiers, opt queryOptions) (string, error) {
	switch typ {
	case "row":
		return generateRowQuery(index, field, identifiers), nil
	case "row-bsi":
		return generateRowBSIQuery(index, field), nil
	case "row-range":
		return generateRowRangeQuery(index, field, identifiers, opt.from, opt.to), nil
	case "count":
		return generateCountQuery(index, field, identifiers), nil
	case "intersect":
		return generateIntersectQuery(index, field, identifiers), nil
	case "union":
		return generateUnionQuery(index, field, identifiers), nil
	case "difference":
		return generateDifferenceQuery(index, field, identifiers), nil
	case "xor":
		return generateXorQuery(index, field, identifiers), nil
	case "groupby":
		return generateGroupByQuery(index, field), nil
	case "topk":
		return generateTopKQuery(index, field, opt.from, opt.to), nil
	default:
		return "", fmt.Errorf("invalid query type: %q", typ)
	}
}

func generateRowQuery(index, field string, identifiers *pilosa.RowIdentifiers) string {
	if len(identifiers.Rows) > 0 {
		return fmt.Sprintf("Row(%s=%d)", field, chooseRowID(identifiers))
	}
	return fmt.Sprintf("Row(%s=%q)", field, chooseRowKey(identifiers))
}

func generateRowBSIQuery(index, field string) string {
	return fmt.Sprintf("Row(%s > 0)", field)
}

func generateRowRangeQuery(index, field string, identifiers *pilosa.RowIdentifiers, from, to time.Time) string {
	if len(identifiers.Rows) > 0 {
		return fmt.Sprintf("Row(%s=%d, from='%s', to='%s')", field, chooseRowID(identifiers), from.Format("2006-01-02T15:04"), to.Format("2006-01-02T15:04"))
	}
	return fmt.Sprintf("Row(%s=%q, from='%s', to='%s')", field, chooseRowKey(identifiers), from.Format("2006-01-02T15:04"), to.Format("2006-01-02T15:04"))
}

func generateRowQueries(index, field string, identifiers *pilosa.RowIdentifiers) string {
	a := make([]string, rand.Intn(9)+1)
	for i := range a {
		a[i] = generateRowQuery(index, field, identifiers)
	}
	return strings.Join(a, ", ")
}

func generateCountQuery(index, field string, identifiers *pilosa.RowIdentifiers) string {
	return fmt.Sprintf("Count(%s)", generateRowQuery(index, field, identifiers))
}

func generateIntersectQuery(index, field string, identifiers *pilosa.RowIdentifiers) string {
	return fmt.Sprintf("Intersect(%s)", generateRowQueries(index, field, identifiers))
}

func generateUnionQuery(index, field string, identifiers *pilosa.RowIdentifiers) string {
	return fmt.Sprintf("Union(%s)", generateRowQueries(index, field, identifiers))
}

func generateDifferenceQuery(index, field string, identifiers *pilosa.RowIdentifiers) string {
	return fmt.Sprintf("Difference(%s)", generateRowQueries(index, field, identifiers))
}

func generateXorQuery(index, field string, identifiers *pilosa.RowIdentifiers) string {
	return fmt.Sprintf("Xor(%s)", generateRowQueries(index, field, identifiers))
}

func generateGroupByQuery(index, field string) string {
	return fmt.Sprintf("GroupBy(Rows(%s))", field)
}

func generateTopKQuery(index, field string, from, to time.Time) string {
	return fmt.Sprintf("TopK(%s, from='%s', to='%s')", field, from.Format("2006-01-02T15:04"), to.Format("2006-01-02T15:04"))
}

// loadFields returns a mapping of index/field names to field info & identifiers.
func loadFields(ctx context.Context, client *pilosa.InternalClient) (map[fieldKey]*fieldInfo, error) {
	indexes, err := client.Schema(ctx)
	if err != nil {
		return nil, err
	}

	m := make(map[fieldKey]*fieldInfo)
	for _, ii := range indexes {
		for _, f := range ii.Fields {
			log.Printf("field: index=%s name=%s type=%s", ii.Name, f.Name, f.Options.Type)

			switch f.Options.Type {
			case "set", "mutex", "time":
				identifiers, err := fetchFieldIDs(ctx, client, ii.Name, f.Name)
				if err != nil {
					return nil, fmt.Errorf("fetch fields: %w", err)
				} else if len(identifiers.Rows) > 0 || len(identifiers.Keys) > 0 {
					m[fieldKey{ii.Name, f.Name}] = &fieldInfo{f, identifiers}
				}

			case "int":
				m[fieldKey{ii.Name, f.Name}] = &fieldInfo{info: f}
			}
		}
	}
	return m, nil
}

// fetchFieldIDs returns a list of field IDs or keys.
func fetchFieldIDs(ctx context.Context, client *pilosa.InternalClient, indexName, fieldName string) (*pilosa.RowIdentifiers, error) {
	resp, err := client.Query(ctx, indexName, &pilosa.QueryRequest{Index: indexName, Query: `Rows(` + fieldName + `)`})
	if err != nil {
		return nil, err
	}

	switch result := resp.Results[0].(type) {
	case *pilosa.RowIdentifiers:
		return result, nil
	case pilosa.RowIdentifiers:
		return &result, nil
	default:
		return nil, fmt.Errorf("unexpected result type: %T", result)
	}
}

func chooseRowID(identifiers *pilosa.RowIdentifiers) uint64 {
	return identifiers.Rows[rand.Intn(len(identifiers.Rows))]
}

func chooseRowKey(identifiers *pilosa.RowIdentifiers) string {
	return identifiers.Keys[rand.Intn(len(identifiers.Keys))]
}

type fieldKey struct {
	index string
	field string
}

type fieldInfo struct {
	info        *pilosa.FieldInfo
	identifiers *pilosa.RowIdentifiers
}

func compareFieldKeys(x, y fieldKey) int {
	if cmp := strings.Compare(x.index, y.index); cmp != 0 {
		return cmp
	}
	return strings.Compare(x.field, y.field)
}

type queryOptions struct {
	from, to time.Time
}
