package main

import (
	"flag"
	"fmt"
	"log"
	"math/rand"
	"os"
	"sync"
	"time"

	"gopkg.in/ini.v1"
	"gordenko.dev/dima/qb/client"
)

const (
	listCumulativeMeasures = 0
	listCumulativePeriods  = 1
	listInstantMeasures    = 2
	listInstantPeriods     = 3
	listCurrentValues      = 4

	methodsQty    = 5
	timeRangesQty = 4
)

var (
	methodCodeToName = []string{
		"listCumulativeMeasures",
		"listCumulativePeriods",
		"listInstantMeasures",
		"listInstantPeriods",
		"listCurrentValues",
	}

	rangeCodeToName = []string{
		"last day",
		"last week",
		"last month",
		"random time range",
	}
)

type WorkerStat struct {
	Queries                 int
	ElapsedTime             time.Duration
	MethodCalls             []int
	ElapsedTimeByMethods    []time.Duration
	TimeRangeCalls          []int
	ElapsedTimeByTimeRanges []time.Duration
}

func main() {
	var (
		iniFileName string
	)

	flag.Usage = func() {
		fmt.Fprint(flag.CommandLine.Output(), helpMessage)
		fmt.Fprint(flag.CommandLine.Output(), configExample)
		fmt.Fprintf(flag.CommandLine.Output(), mainUsage, os.Args[0])
		flag.PrintDefaults()
	}
	flag.StringVar(&iniFileName, "c", "loadtest.ini", "path to *.ini config file")
	flag.Parse()

	config, err := loadConfig(iniFileName)
	if err != nil {
		log.Fatalln(err)
	}

	rand.Seed(time.Now().UnixNano())

	metrics, err := readMetricInfo(config.MetricsInfo)
	if err != nil {
		log.Fatalln(err)
	}

	var (
		wg             = new(sync.WaitGroup)
		stats          = make([]*WorkerStat, config.Connections)
		queryGenerator = NewRandomQueryGenerator(RandomQueryGeneratorOptions{
			Metrics: metrics,
			// call method probabilitites
			ListCurrentValuesProbability: 50, // current values / others
			ListPeriodsProbability:       80, // periods / measures
			// time range probabilities
			TimeRangeProbabilities: []int{
				82, // last day
				12, // last week
				3,  // last month
				3,  // any range
			},
		})
	)

	for i := range stats {
		stats[i] = &WorkerStat{
			MethodCalls:             make([]int, methodsQty),
			ElapsedTimeByMethods:    make([]time.Duration, methodsQty),
			TimeRangeCalls:          make([]int, timeRangesQty),
			ElapsedTimeByTimeRanges: make([]time.Duration, timeRangesQty),
		}
	}

	t1 := time.Now()

	for i := range config.Connections {
		wg.Add(1)
		go func(stat *WorkerStat) {
			defer wg.Done()

			conn, err := client.Connect(config.DatabaseAddr)
			if err != nil {
				log.Fatalln(err)
			}
			defer conn.Close()

			for range config.RequestsPerConn {
				err := execQuery(conn, queryGenerator, stat)
				if err != nil {
					log.Println(err)
				}
			}
		}(stats[i])
	}

	wg.Wait()

	testingTime := time.Since(t1)

	var (
		methodCalls             = make([]int, methodsQty)
		elapsedTimeByMethods    = make([]time.Duration, methodsQty)
		timeRangeCalls          = make([]int, timeRangesQty)
		elapsedTimeByTimeRanges = make([]time.Duration, timeRangesQty)
		totalElapsedTime        time.Duration
		totalQueries            int
		avgTimePerQuery         time.Duration
		rps                     float64
	)

	for _, stat := range stats {
		totalElapsedTime += stat.ElapsedTime
		totalQueries += stat.Queries

		for i, elapsedTime := range stat.ElapsedTimeByMethods {
			elapsedTimeByMethods[i] += elapsedTime
		}
		for i, qty := range stat.MethodCalls {
			methodCalls[i] += qty
		}
		for i, elapsedTime := range stat.ElapsedTimeByTimeRanges {
			elapsedTimeByTimeRanges[i] += elapsedTime
		}
		for i, qty := range stat.TimeRangeCalls {
			timeRangeCalls[i] += qty
		}
	}

	avgTimePerQuery = totalElapsedTime / time.Duration(totalQueries)
	rps = float64(config.Connections*config.RequestsPerConn) / testingTime.Seconds()

	fmt.Printf(`TEST RESULTS:
Time: %.0f seconds
Connections: %d
Requests per conn: %d
Total requests: %d
AVG request time: %v
RPS: %d

`,
		testingTime.Seconds(), config.Connections, config.RequestsPerConn,
		totalQueries, avgTimePerQuery, int(rps))

	for i, calls := range methodCalls {
		totalElapsedTimeByMethod := elapsedTimeByMethods[i]

		methodPercent := float64(calls*100) / float64(totalQueries)

		fmt.Printf("%s: %d (%.1f%%), AVG request time: %v\n",
			methodCodeToName[i], calls, methodPercent,
			totalElapsedTimeByMethod/time.Duration(calls))
	}

	fmt.Println()

	for i, calls := range timeRangeCalls {
		totalElapsedTimeByTimeRange := elapsedTimeByTimeRanges[i]

		timeRangePercent := float64(calls*100) / float64(totalQueries-methodCalls[listCurrentValues])

		fmt.Printf("%s: %d (%.1f%%), AVG request time: %v\n",
			rangeCodeToName[i], calls, timeRangePercent,
			totalElapsedTimeByTimeRange/time.Duration(calls))
	}
}

// CONFIG FILE

const mainUsage = `Usage:
	%s -c path/to/config.ini

`

const helpMessage = `Diploma project. Load test. Version: 1.0
created by Dmytro Gordenko, 1.e4.kc6@gmail.com
`

const configExample = `
loadtest.ini example:

databaseAddr    = :12345
metricsInfo     = ../../datadir/metrics.info
connections     = 1000
requestsPerConn = 500

`

type Config struct {
	DatabaseAddr    string
	MetricsInfo     string
	Connections     int
	RequestsPerConn int
}

func (s Config) String() string {
	return fmt.Sprintf(`starting options:
databaseAddr    = %s
metricsInfo     = %s
connections     = %d
requestsPerConn = %d
`,
		s.DatabaseAddr, s.MetricsInfo, s.Connections, s.RequestsPerConn)
}

func loadConfig(iniFileName string) (_ Config, err error) {
	file, err := ini.Load(iniFileName)
	if err != nil {
		return
	}

	conf := Config{}

	top := file.Section("")

	conf.DatabaseAddr = top.Key("databaseAddr").String()
	conf.MetricsInfo = top.Key("metricsInfo").String()

	conf.Connections, err = top.Key("connections").Int()
	if err != nil {
		err = fmt.Errorf("'connections' option is required in config file")
		return
	}
	conf.RequestsPerConn, err = top.Key("requestsPerConn").Int()
	if err != nil {
		err = fmt.Errorf("'requestsPerConn' option is required in config file")
		return
	}
	return conf, nil
}
