package checker

import (
	"context"
	"fmt"
	"math"
	"sort"
	"sync"
	"time"

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

const reservoirSize = 1024 * 100

var _ types.GomegaMatcher = &latencyChecker{}
var _ types.OracleMatcher = &latencyChecker{}

type LatencyStaticsMethod string

const (
	MeanLatency       LatencyStaticsMethod = "mean"
	MaxLatency        LatencyStaticsMethod = "max"
	MinLatency        LatencyStaticsMethod = "min"
	PercentileLatency LatencyStaticsMethod = "percentile"
)

// 当前的时间窗口（window1）采样数据的统计值（mean/min/max/percentile）和上一个（offset）时间窗口(window2)采样数据统计值的对比
// window,offset 代表的是样本的数量，是逻辑上的时间概念
type latencyChecker struct {
	h          *Histogram
	ctx        context.Context
	window1    int
	window2    int
	offset     int
	percentile float64
	diff       float64
	// report log when matcher fail
	results []checkerRes
}

type checkerRes struct {
	latencyNow float64
	latencyBef float64
	time       time.Time
}

func newLatencyCheck(h *Histogram) *latencyChecker {
	return &latencyChecker{h: h}
}

func (l *latencyChecker) MatchMayChangeInTheFuture(actual interface{}) bool {
	if l.ctx != nil {
		select {
		case <-l.ctx.Done():
			log.Info("MatchMayChangeInTheFuture context done", zap.String("histogram name", l.h.Name))
			return false
		default:
		}
	}
	return true
}

// mean window offset
func (l *latencyChecker) Match(actual interface{}) (success bool, err error) {
	if l.ctx != nil {
		select {
		case <-l.ctx.Done():
			log.Info("Match context done", zap.String("histogram name", l.h.Name))
			return false, nil
		default:
		}
	}
	var now, bef float64
	t := time.Now()
	method := actual.(LatencyStaticsMethod)
	switch method {
	case MeanLatency:
		now = l.h.mean(l.window1, 0)
		bef = l.h.mean(l.window2, l.offset)
	case MaxLatency:
		now = float64(l.h.max(l.window1, 0))
		bef = float64(l.h.max(l.window2, l.offset))
	case MinLatency:
		now = float64(l.h.min(l.window1, 0))
		bef = float64(l.h.min(l.window2, l.offset))
	case PercentileLatency:
		now = l.h.percentile(l.window1, 0, l.percentile)
		bef = l.h.percentile(l.window2, l.offset, l.percentile)
	default:
		return true, nil
	}
	l.results = append(l.results, checkerRes{latencyNow: now, latencyBef: bef, time: t})
	log.Info("check latency", zap.String("Name", l.h.Name), zap.Int("window1", l.window1), zap.Int("window2", l.window2), zap.Int("offset", l.offset),
		zap.Float64("now", now), zap.Float64("bef", bef), zap.Float64("allow max latency", bef*(1+l.diff)))
	return now > bef*(1+l.diff), nil
}

func (l *latencyChecker) FailureMessage(actual interface{}) (message string) {
	return l.getFailureMessage(actual, false)

}
func (l *latencyChecker) NegatedFailureMessage(actual interface{}) (message string) {
	return l.getFailureMessage(actual, true)
}
func (l *latencyChecker) getFailureMessage(method interface{}, isNegative bool) (message string) {
	var errmsg string
	if isNegative {
		errmsg = "false"
	} else {
		errmsg = "true"
	}
	return fmt.Sprintf("Expected\n %.4f > %.4f*(1+%.2f)[%.4f] \nto match the expected result of latency checker [histogram name: %s, statistic method: %s, window1: %d, window2: %d, offset: %d, diff: %.2f, percentile: %.2f]\n%s\n",
		l.results[len(l.results)-1].latencyNow,
		l.results[len(l.results)-1].latencyBef,
		l.diff,
		l.results[len(l.results)-1].latencyBef*(1+l.diff),
		l.h.Name,
		method.(LatencyStaticsMethod),
		l.window1,
		l.window2,
		l.offset,
		l.diff,
		l.percentile,
		errmsg)
}

func (l *latencyChecker) WithCtx(ctx context.Context) *latencyChecker {
	l.ctx = ctx
	return l
}

type sample struct {
	v int64
	// unix timestamp nano
	t int64
}

type Histogram struct {
	Name   string
	values []sample
	mutex  sync.Mutex
}

func NewHistogram(name string) *Histogram {
	values := make([]sample, 0, reservoirSize)
	return &Histogram{Name: name, values: values}
}

func (h *Histogram) Update(v int64) {
	h.mutex.Lock()
	defer h.mutex.Unlock()
	n := len(h.values)
	// regenerate slice , keep 1/10 origin data
	if n >= reservoirSize {
		log.Info("regenerate slice")
		values := h.values[n-(n/10+1) : n]
		h.clear()
		for _, v := range values {
			h.append(v)
		}
	}
	s := sample{v, time.Now().UnixNano()}
	h.append(s)
	// log.Info("values", zap.String("Name", h.Name), zap.Any("currency sample", s.v), zap.Any("values", h.values))
}

func (h *Histogram) append(s sample) {
	n := len(h.values)
	h.values = h.values[0 : n+1]
	h.values[n] = s
}

func (h *Histogram) clear() {
	h.values = h.values[:0]
}

// TODO offset from time or int
func (h *Histogram) getValues(size, offset int) []int64 {
	h.mutex.Lock()
	defer h.mutex.Unlock()
	vals := h.values
	if len(h.values) >= offset+size {
		vals = h.values[len(h.values)-offset-size : len(h.values)-offset]
	}
	values := make([]int64, len(vals))
	for i, v := range vals {
		values[i] = v.v
	}
	log.Info("latency values", zap.String("Name", h.Name), zap.Int64s("latency", values))
	return values
}

func (h *Histogram) sum(size, offset int) int64 {
	var sum int64
	values := h.getValues(size, offset)
	for _, v := range values {
		sum += v
	}
	return sum
}

func (h *Histogram) max(size, offset int) int64 {
	values := h.getValues(size, offset)
	if 0 == len(values) {
		return 0
	}
	var max int64 = math.MinInt64
	for _, v := range values {
		if max < v {
			max = v
		}
	}
	return max
}

func (h *Histogram) min(size, offset int) int64 {
	values := h.getValues(size, offset)
	if 0 == len(values) {
		return 0
	}
	var max int64 = math.MaxInt64
	for _, v := range values {
		if max > v {
			max = v
		}
	}
	return max
}

func (h *Histogram) mean(size, offset int) float64 {
	var sum int64
	values := h.getValues(size, offset)
	if 0 == len(values) {
		return 0
	}
	for _, v := range values {
		sum += v
	}
	return float64(sum) / float64(len(values))
}

func (h *Histogram) percentile(size, offset int, p float64) float64 {
	var score float64
	values := h.getValues(size, offset)
	length := len(values)
	if length > 0 {
		sort.Sort(int64Slice(values))
		pos := p * float64(length+1)
		if pos < 1.0 {
			score = float64(values[0])
		} else if pos >= float64(length) {
			score = float64(values[length-1])
		} else {
			lower := float64(values[int(pos)-1])
			upper := float64(values[int(pos)])
			score = lower + (pos-math.Floor(pos))*(upper-lower)
		}
	}
	return score
}

type int64Slice []int64

func (p int64Slice) Len() int           { return len(p) }
func (p int64Slice) Less(i, j int) bool { return p[i] < p[j] }
func (p int64Slice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

// 一般的使用方式
// 1. gomega.Consistently(MeanLatency,30*time.Minute, 30*time.Second).ShouldNot(
//		LatencyIncreasedMoreThanOffset(h,20,20,10,0.2,0).WithCtx(ctx))
// 当前 20 个 query 的平均值和 10 个 query 之前的 20 个 query 的平均值对比
// 2. gomega.Consistently(MeanLatency,30*time.Minute, 30*time.Second).ShouldNot(
//		LatencyIncreasedMoreThanOffset(h,1,20,1,0.2,0).WithCtx(ctx))
// 反应 query latency 突变的情况，如 https://github.com/pingcap/tiflash/issues/4473
func LatencyIncreasedMoreThanOffset(h *Histogram, window1, window2, offset int, diff, percentile float64) *latencyChecker {
	l := newLatencyCheck(h)
	l.window1 = window1
	l.window2 = window2
	l.offset = offset
	l.diff = diff
	l.percentile = percentile
	return l
}
