package memmetrics

import (
	"math"
	"sort"
	"time"
)

// SplitRatios provides simple anomaly detection for requests latencies.
// it splits values into good or bad category based on the threshold and the median value.
// If all values are not far from the median, it will return all values in 'good' set.
// Precision is the smallest value to consider, e.g. if set to millisecond, microseconds will be ignored.
func SplitLatencies(values []time.Duration, precision time.Duration) (good map[time.Duration]bool, bad map[time.Duration]bool) {
	// Find the max latency M and then map each latency L to the ratio L/M and then call SplitFloat64
	v2r := map[float64]time.Duration{}
	ratios := make([]float64, len(values))
	m := maxTime(values)
	for i, v := range values {
		ratio := float64(v/precision+1) / float64(m/precision+1) // +1 is to avoid division by 0
		v2r[ratio] = v
		ratios[i] = ratio
	}
	good, bad = make(map[time.Duration]bool), make(map[time.Duration]bool)
	// Note that multiplier makes this function way less sensitive than ratios detector, this is to avoid noise.
	vgood, vbad := SplitFloat64(2, 0, ratios)
	for r, _ := range vgood {
		good[v2r[r]] = true
	}
	for r, _ := range vbad {
		bad[v2r[r]] = true
	}
	return good, bad
}

// SplitRatios provides simple anomaly detection for ratio values, that are all in the range [0, 1]
// it splits values into good or bad category based on the threshold and the median value.
// If all values are not far from the median, it will return all values in 'good' set.
func SplitRatios(values []float64) (good map[float64]bool, bad map[float64]bool) {
	return SplitFloat64(1.5, 0, values)
}

// SplitFloat64 provides simple anomaly detection for skewed data sets with no particular distribution.
// In essense it applies the formula if(v > median(values) + threshold * medianAbsoluteDeviation) -> anomaly
// There's a corner case where there are just 2 values, so by definition there's no value that exceeds the threshold.
// This case is solved by introducing additional value that we know is good, e.g. 0. That helps to improve the detection results
// on such data sets.
func SplitFloat64(threshold, sentinel float64, values []float64) (good map[float64]bool, bad map[float64]bool) {
	good, bad = make(map[float64]bool), make(map[float64]bool)
	var newValues []float64
	if len(values)%2 == 0 {
		newValues = make([]float64, len(values)+1)
		copy(newValues, values)
		// Add a sentinel endpoint so we can distinguish outliers better
		newValues[len(newValues)-1] = sentinel
	} else {
		newValues = values
	}

	m := median(newValues)
	mAbs := medianAbsoluteDeviation(newValues)
	for _, v := range values {
		if v > (m+mAbs)*threshold {
			bad[v] = true
		} else {
			good[v] = true
		}
	}
	return good, bad
}

func median(values []float64) float64 {
	vals := make([]float64, len(values))
	copy(vals, values)
	sort.Float64s(vals)
	l := len(vals)
	if l%2 != 0 {
		return vals[l/2]
	}
	return (vals[l/2-1] + vals[l/2]) / 2.0
}

func medianAbsoluteDeviation(values []float64) float64 {
	m := median(values)
	distances := make([]float64, len(values))
	for i, v := range values {
		distances[i] = math.Abs(v - m)
	}
	return median(distances)
}

func maxTime(vals []time.Duration) time.Duration {
	val := vals[0]
	for _, v := range vals {
		if v > val {
			val = v
		}
	}
	return val
}
