package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"os"
	"time"

	diploma "gordenko.dev/dima/qb"
	"gordenko.dev/dima/qb/client"
	"gordenko.dev/dima/qb/proto"
)

// METRICS INFO

type MetricInfo struct {
	MetricID   uint32             `json:"metricID"`
	MetricType diploma.MetricType `json:"metricType"`
	FracDigits int                `json:"fracDigits"`
	Since      int64              `json:"since"`
	Until      int64              `json:"until"`
	Qty        int                `json:"qty"`
}

func readMetricInfo(fileName string) (list []MetricInfo, err error) {
	buf, err := os.ReadFile(fileName)
	if err != nil {
		return
	}

	err = json.Unmarshal(buf, &list)
	return
}

// RANDOM QUERY GENERATOR

type QueryRecipe struct {
	MetricID  uint32
	MetricIDs []uint32
	Method    int
	RangeCode int
	Since     uint32
	Until     uint32
	GroupBy   diploma.GroupBy
}

type RandomQueryGenerator struct {
	metrics                      []MetricInfo
	groupByOptions               []diploma.GroupBy
	listCurrentValuesProbability int
	listPeriodsProbability       int
	timeRangeProbDistribution    []int
}

type RandomQueryGeneratorOptions struct {
	Metrics                      []MetricInfo
	ListCurrentValuesProbability int
	ListPeriodsProbability       int
	TimeRangeProbabilities       []int
}

func NewRandomQueryGenerator(opt RandomQueryGeneratorOptions) *RandomQueryGenerator {
	if opt.ListCurrentValuesProbability >= 100 {
		panic(fmt.Sprintf("wrong ListCurrentValuesProbability: %d", opt.ListCurrentValuesProbability))
	}

	if opt.ListPeriodsProbability >= 100 {
		panic(fmt.Sprintf("wrong ListPeriodsProbability: %d", opt.ListPeriodsProbability))
	}
	// check total time range propability
	var totalTimeRangeProbability int
	for _, p := range opt.TimeRangeProbabilities {
		totalTimeRangeProbability += p
	}
	if totalTimeRangeProbability != 100 {
		panic(fmt.Sprintf("total time range probabilities != 100: %d", totalTimeRangeProbability))
	}

	// create time range probability distribution
	timeRangeProbDistribution := make([]int, len(opt.TimeRangeProbabilities))
	timeRangeProbDistribution[0] = opt.TimeRangeProbabilities[0]
	for i := 1; i < len(opt.TimeRangeProbabilities); i++ {
		timeRangeProbDistribution[i] = timeRangeProbDistribution[i-1] + opt.TimeRangeProbabilities[i]
	}

	return &RandomQueryGenerator{
		metrics: opt.Metrics,
		groupByOptions: []diploma.GroupBy{
			diploma.GroupByHour,
			diploma.GroupByDay,
			diploma.GroupByMonth,
		},
		listCurrentValuesProbability: opt.ListCurrentValuesProbability,
		listPeriodsProbability:       opt.ListPeriodsProbability,
		timeRangeProbDistribution:    timeRangeProbDistribution,
	}
}

func (s *RandomQueryGenerator) GetQueryRecipe() QueryRecipe {
	metric := s.getRandomMetric()

	num := rand.Intn(100)
	if num < s.listCurrentValuesProbability {
		qty := 5 + rand.Intn(100) // від 5 до 105
		return QueryRecipe{
			MetricIDs: s.listRandomUniqueMetricIDs(qty),
			Method:    listCurrentValues,
		}
	} else {
		if metric.MetricType == diploma.Cumulative {
			num = rand.Intn(100)
			if num < s.listPeriodsProbability {
				groupBy := s.groupByOptions[rand.Intn(len(s.groupByOptions))]

				var (
					minDays = 1
					maxDays = 7
				)

				if groupBy == diploma.GroupByDay {
					minDays = 1
					maxDays = 30
				} else if groupBy == diploma.GroupByMonth {
					minDays = 1
					maxDays = 30
				}

				rangeCode, since, until := s.getRandomTimeRange(
					metric.Since, metric.Until, minDays, maxDays)

				return QueryRecipe{
					MetricID:  metric.MetricID,
					Method:    listCumulativePeriods,
					RangeCode: rangeCode,
					Since:     uint32(since),
					Until:     uint32(until),
					GroupBy:   groupBy,
				}
			} else {
				var (
					minDays = 1
					maxDays = 3
				)

				rangeCode, since, until := s.getRandomTimeRange(
					metric.Since, metric.Until, minDays, maxDays)

				return QueryRecipe{
					MetricID:  metric.MetricID,
					Method:    listCumulativeMeasures,
					RangeCode: rangeCode,
					Since:     uint32(since),
					Until:     uint32(until),
				}
			}
		} else {
			num = rand.Intn(100)
			if num < s.listPeriodsProbability {
				groupBy := s.groupByOptions[rand.Intn(len(s.groupByOptions))]

				var (
					minDays = 1
					maxDays = 7
				)

				if groupBy == diploma.GroupByDay {
					minDays = 1
					maxDays = 30
				} else if groupBy == diploma.GroupByMonth {
					minDays = 1
					maxDays = 30
				}

				rangeCode, since, until := s.getRandomTimeRange(
					metric.Since, metric.Until, minDays, maxDays)

				return QueryRecipe{
					MetricID:  metric.MetricID,
					Method:    listInstantPeriods,
					RangeCode: rangeCode,
					Since:     uint32(since),
					Until:     uint32(until),
					GroupBy:   groupBy,
				}
			} else {
				var (
					minDays = 1
					maxDays = 3
				)

				rangeCode, since, until := s.getRandomTimeRange(
					metric.Since, metric.Until, minDays, maxDays)

				return QueryRecipe{
					MetricID:  metric.MetricID,
					Method:    listInstantMeasures,
					RangeCode: rangeCode,
					Since:     uint32(since),
					Until:     uint32(until),
				}
			}
		}
	}

}

// Генерує випадковий набір унікальних metricID з [1, 100]
func (s *RandomQueryGenerator) listRandomUniqueMetricIDs(count int) []uint32 {
	// переставляю індекси у випадковому порядку
	indexes := rand.Perm(len(s.metrics))
	// копіюю metricID із перших випадкових індексів
	metricIDs := make([]uint32, count)
	for i := range count {
		metricIDs[i] = s.metrics[indexes[i]].MetricID
	}
	return metricIDs
}

const (
	secondsPerDay   = 86400
	dayRange        = 0
	weekRange       = 1
	monthRange      = 2
	randomTimeRange = 3
)

// Випадковий часовий діапазон
func (s *RandomQueryGenerator) getRandomTimeRange(start, end int64, minDays, maxDays int) (int, int64, int64) {
	var (
		since     int64
		until     int64
		num       = rand.Intn(100)
		rangeCode int
		threshold int
	)
	for rangeCode, threshold = range s.timeRangeProbDistribution {
		if num < threshold {
			break
		}
	}

	switch rangeCode {
	case dayRange:
		since = end - secondsPerDay
		until = end

	case weekRange:
		since = end - 7*secondsPerDay
		until = end

	case monthRange:
		since = end - 30*secondsPerDay
		until = end

	case randomTimeRange:
		if start == end {
			return rangeCode, start, end
		}
		// Випадковий момент часу для since
		since = start + rand.Int63n(end-start)
		// Випадкова тривалість у днях (але не виходити за межу end)
		durationInDays := minDays + rand.Intn(maxDays-minDays)

		until = since + int64(durationInDays)*secondsPerDay
		if until > end {
			until = end
		}
	}
	return rangeCode, since, until
}

func (s *RandomQueryGenerator) getRandomMetric() MetricInfo {
	return s.metrics[rand.Intn(len(s.metrics))]
}

// EXECUTE QUERY

func execQuery(conn *client.Connection, queryGenerator *RandomQueryGenerator, stat *WorkerStat) (err error) {
	recipe := queryGenerator.GetQueryRecipe()

	var elapsedTime time.Duration

	switch recipe.Method {
	case listCurrentValues:
		t1 := time.Now()
		_, err := conn.ListCurrentValues(recipe.MetricIDs)
		elapsedTime = time.Since(t1)
		stat.ElapsedTime += elapsedTime
		stat.Queries++
		stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime
		stat.MethodCalls[recipe.Method]++
		if err != nil {
			return fmt.Errorf("ListCurrentValues: %s", err)
		}

	case listInstantMeasures:
		t1 := time.Now()
		_, err := conn.ListInstantMeasures(proto.ListInstantMeasuresReq{
			MetricID: recipe.MetricID,
			Since:    recipe.Since,
			Until:    recipe.Until,
		})
		elapsedTime = time.Since(t1)
		stat.ElapsedTime += elapsedTime
		stat.Queries++
		stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime
		stat.MethodCalls[recipe.Method]++
		stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime
		stat.TimeRangeCalls[recipe.RangeCode]++
		if err != nil {
			return fmt.Errorf("ListInstantMeasures(%d): %s",
				recipe.MetricID, err)
		}

	case listCumulativeMeasures:
		t1 := time.Now()
		_, err := conn.ListCumulativeMeasures(proto.ListCumulativeMeasuresReq{
			MetricID: recipe.MetricID,
			Since:    recipe.Since,
			Until:    recipe.Until,
		})
		elapsedTime = time.Since(t1)
		stat.ElapsedTime += elapsedTime
		stat.Queries++
		stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime
		stat.MethodCalls[recipe.Method]++
		stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime
		stat.TimeRangeCalls[recipe.RangeCode]++
		if err != nil {
			return fmt.Errorf("ListCumulativeMeasures(%d): %s",
				recipe.MetricID, err)
		}

	case listInstantPeriods:
		since := time.Unix(int64(recipe.Since), 0)
		until := time.Unix(int64(recipe.Until), 0)
		//
		t1 := time.Now()
		_, err := conn.ListInstantPeriods(proto.ListInstantPeriodsReq{
			MetricID: recipe.MetricID,
			Since: proto.TimeBound{
				Year:  since.Year(),
				Month: since.Month(),
				Day:   since.Day(),
			},
			Until: proto.TimeBound{
				Year:  until.Year(),
				Month: until.Month(),
				Day:   until.Day(),
			},
			GroupBy:        recipe.GroupBy,
			AggregateFuncs: diploma.AggregateMin | diploma.AggregateMax | diploma.AggregateAvg,
		})
		elapsedTime = time.Since(t1)
		stat.ElapsedTime += elapsedTime
		stat.Queries++
		stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime
		stat.MethodCalls[recipe.Method]++
		stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime
		stat.TimeRangeCalls[recipe.RangeCode]++
		if err != nil {
			return fmt.Errorf("ListInstantPeriods(%d): %s",
				recipe.MetricID, err)
		}

	case listCumulativePeriods:
		since := time.Unix(int64(recipe.Since), 0)
		until := time.Unix(int64(recipe.Until), 0)
		//
		t1 := time.Now()
		_, err := conn.ListCumulativePeriods(proto.ListCumulativePeriodsReq{
			MetricID: recipe.MetricID,
			Since: proto.TimeBound{
				Year:  since.Year(),
				Month: since.Month(),
				Day:   since.Day(),
			},
			Until: proto.TimeBound{
				Year:  until.Year(),
				Month: until.Month(),
				Day:   until.Day(),
			},
			GroupBy: recipe.GroupBy,
		})
		elapsedTime = time.Since(t1)
		stat.ElapsedTime += elapsedTime
		stat.Queries++
		stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime
		stat.MethodCalls[recipe.Method]++
		stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime
		stat.TimeRangeCalls[recipe.RangeCode]++
		if err != nil {
			return fmt.Errorf("ListCumulativePeriods(%d): %s",
				recipe.MetricID, err)
		}
	}
	return
}
