package metric

import (
	"fmt"
	"go.uber.org/zap"
	"math"
	"time"
)

type ResourceGroupNameType string

type basePoint struct {
	ts time.Time
}

func (t *basePoint) GetTs() time.Time {
	return t.ts
}

type CPUPoint struct {
	basePoint
	cpu float64
}

type OOMPoint struct {
	basePoint
	oom uint
}

func (o *OOMPoint) GetOOM() uint {
	return o.oom
}

func (o *OOMPoint) ToString() string {
	return fmt.Sprintf("{%v,%v}", o.GetTs(), o.oom)
}

type TaskCntPoint struct {
	basePoint
	totalTaskCnt     uint
	tenantTaskCntMap map[ResourceGroupNameType]uint
}

func (t *TaskCntPoint) GetTaskCnt(rgName ResourceGroupNameType) (uint, bool) {
	cnt, ok := t.tenantTaskCntMap[rgName]
	return cnt, ok
}

type RUPoint struct {
	basePoint
	// Total RU of this pods.
	totalRU float64
	// map: tenant => RU
	tenantRUMap map[ResourceGroupNameType]float64
}

// func (p *CPUPoint) MarshalLogObject(enc zapcore.ObjectEncoder) error {
//     enc.AddString("ts", p.basePoint.ts.Format("2006-01-02 15:04:05"))
//     enc.AddFloat64("cpu", p.cpu)
//     return nil
// }

func (p *PodsCPUMetricWindow) ToString() string {
	var res string
	for podName, window := range *p {
		var windowStr string
		for _, point := range window {
			windowStr += fmt.Sprintf("(%v,%v)", point.ts, point.cpu)
		}
		res += fmt.Sprintf(";%s: %s", podName, windowStr)
	}
	return res
}

func (p *PodsOOMMetricWindow) ToString() string {
	var res string
	for podName, window := range *p {
		var windowStr string
		for _, point := range window {
			windowStr += fmt.Sprintf("(%v,%v)", point.ts, point.oom)
		}
		res += fmt.Sprintf(";%s: %s", podName, windowStr)
	}
	return res
}

func (p *PodsTaskCntMetricWindow) ToString() string {
	var res string
	for podName, window := range *p {
		var windowStr string
		for _, point := range window {
			windowStr += fmt.Sprintf("(%v,%v)", point.ts, point.tenantTaskCntMap)
		}
		res += fmt.Sprintf(";%s: %s", podName, windowStr)
	}
	return res
}

func (p *PodsRUMetricWindow) ToString() string {
	var res string
	for podName, window := range *p {
		var windowStr string
		for _, point := range window {
			windowStr += fmt.Sprintf("(%v,%v,%v)", point.ts, point.totalRU, point.tenantRUMap)
		}
		res += fmt.Sprintf(";%s: %s", podName, windowStr)
	}
	return res
}

type CPUMetricWindow []*CPUPoint
type OOMMetricWindow []*OOMPoint
type TaskCntMetricWindow []*TaskCntPoint
type RUMetricWindow []*RUPoint

func (w *CPUMetricWindow) GetAvgCPU() float64 {
	var t float64
	for _, p := range *w {
		t += p.cpu
	}
	return t / float64(len(*w))
}

func (w *OOMMetricWindow) ToString() (res string) {
	for _, point := range *w {
		res += point.ToString()
	}
	return res
}

func (w *CPUMetricWindow) DeepCopy() CPUMetricWindow {
	newWindow := make(CPUMetricWindow, 0, len(*w))
	for _, point := range *w {
		newWindow = append(newWindow, point)
	}
	return newWindow
}
func (w *TaskCntMetricWindow) DeepCopy() TaskCntMetricWindow {
	newWindow := make(TaskCntMetricWindow, 0, len(*w))
	for _, point := range *w {
		newWindow = append(newWindow, point)
	}
	return newWindow
}
func (w *OOMMetricWindow) DeepCopy() OOMMetricWindow {
	newWindow := make(OOMMetricWindow, 0, len(*w))
	for _, point := range *w {
		newWindow = append(newWindow, point)
	}
	return newWindow
}
func (w *RUMetricWindow) DeepCopy() RUMetricWindow {
	newWindow := make(RUMetricWindow, 0, len(*w))
	for _, point := range *w {
		newWindow = append(newWindow, point)
	}
	return newWindow
}

// TODO generics
func RemoveToFitsWindowForCPU(now time.Time, windowSec uint, windowData CPUMetricWindow) CPUMetricWindow {
	left := now.Add(time.Duration(-windowSec) * time.Second)
	resWindowData := make(CPUMetricWindow, 0, len(windowData))
	for i, point := range windowData {
		if point.GetTs().After(left) {
			return windowData[i:]
		}
	}
	return resWindowData[:0]
}
func RemoveToFitsWindowForTaskCnt(now time.Time, windowSec uint, windowData TaskCntMetricWindow) TaskCntMetricWindow {
	left := now.Add(time.Duration(-windowSec) * time.Second)
	resWindowData := make(TaskCntMetricWindow, 0, len(windowData))
	for i, point := range windowData {
		if point.GetTs().After(left) {
			return windowData[i:]
		}
	}
	return resWindowData[:0]
}
func RemoveToFitsWindowForOOM(now time.Time, windowSec uint, windowData OOMMetricWindow) OOMMetricWindow {
	left := now.Add(time.Duration(-windowSec) * time.Second)
	resWindowData := make(OOMMetricWindow, 0, len(windowData))
	for i, point := range windowData {
		if point.GetTs().After(left) {
			return windowData[i:]
		}
	}
	return resWindowData[:0]
}
func RemoveToFitsWindowForRU(now time.Time, windowSec uint, windowData RUMetricWindow) RUMetricWindow {
	left := now.Add(time.Duration(-windowSec) * time.Second)
	resWindowData := make(RUMetricWindow, 0, len(windowData))
	for i, point := range windowData {
		if point.GetTs().After(left) {
			return windowData[i:]
		}
	}
	return resWindowData[:0]
}

type PodsCPUMetricWindow map[string]CPUMetricWindow
type PodsOOMMetricWindow map[string]OOMMetricWindow
type PodsTaskCntMetricWindow map[string]TaskCntMetricWindow
type PodsRUMetricWindow map[string]RUMetricWindow

func IsTaskCntWindowValid(window uint, interval uint, rgName ResourceGroupNameType, metricWindow TaskCntMetricWindow) (int, error) {
	var validMetricPoint int
	for _, point := range metricWindow {
		if _, ok := point.tenantTaskCntMap[rgName]; ok {
			validMetricPoint++
		}
	}
	if validMetricPoint == 0 {
		return 0, nil
	}
	return validMetricPoint, IsWindowValid(window, interval, validMetricPoint)
}
func IsRUWindowValid(window uint, interval uint, rgName ResourceGroupNameType, metricWindow RUMetricWindow) (int, error) {
	var validMetricPoint int
	for _, point := range metricWindow {
		if _, ok := point.tenantRUMap[rgName]; ok {
			validMetricPoint++
		}
	}
	if validMetricPoint == 0 {
		return 0, nil
	}
	return validMetricPoint, IsWindowValid(window, interval, validMetricPoint)
}

func IsWindowValid(window uint, interval uint, metricNum int) error {
	requiredMetricPoint := window / interval
	if requiredMetricPoint <= 1 {
		return fmt.Errorf("invalid requiredMetricPoint: %v, metric window: %v, metric interval: %v",
			requiredMetricPoint, window, interval)
	}
	requiredMetricPoint = uint(math.Ceil(float64(requiredMetricPoint) * 0.8))
	if requiredMetricPoint < 2 {
		requiredMetricPoint = 2
	}
	if metricNum < int(requiredMetricPoint) {
		return fmt.Errorf("not enough metric point, cur: %v, required: %v",
			metricNum, requiredMetricPoint)
	}
	return nil
}

func GetRURate(rgName ResourceGroupNameType, window RUMetricWindow) (res float64, msg string) {
	var (
		totalRU  float64
		tenantRU float64
	)
	for _, point := range window {
		totalRU += point.totalRU
		if ru, ok := point.tenantRUMap[rgName]; ok {
			tenantRU += ru
		}
	}
	if totalRU <= 0.0 {
		msg = fmt.Sprintf("totalRU is zero(%v)", totalRU)
		return 0.0, msg
	}
	res = tenantRU / totalRU
	if res > 1.0 {
		msg = fmt.Sprintf("invalid tenantRU(%v) or totalRU(%v), res: %v", tenantRU, totalRU, res)
		return 0.0, msg
	}
	return res, ""
}

func GetHighestRUUsageTenant(window RUMetricWindow, logger *zap.Logger) ResourceGroupNameType {
	type RUPair struct {
		firstTs time.Time
		firstRU *float64

		lastTs time.Time
		lastRU *float64
	}
	// point-1             point-2          ....
	//  total: ru          total: ru
	//  map: <t1, ru>      map: <t1, ru>
	//       <t2, ru>           <t3, ru>
	// =====>
	// map: <rgName, <firstRU, lastRU>>
	record := make(map[ResourceGroupNameType]*RUPair)
	for _, point := range window {
		for rgName, ru := range point.tenantRUMap {
			tmpRU := ru
			curRUPair, ok := record[rgName]
			if !ok {
				record[rgName] = &RUPair{
					firstTs: point.GetTs(),
					firstRU: &tmpRU,
				}
				continue
			}
			if curRUPair.lastRU == nil || point.GetTs().After(curRUPair.lastTs) {
				curRUPair.lastTs = point.GetTs()
				curRUPair.lastRU = &tmpRU
			}
		}
	}

	var (
		maxRURGName ResourceGroupNameType
		maxRUUsage  float64
		msg         string
	)

	for rgName, pair := range record {
		var (
			firstRU float64
			lastRU  float64
		)
		if pair.firstRU != nil {
			firstRU = *pair.firstRU
		}
		if pair.lastRU != nil {
			lastRU = *pair.lastRU
		}
		msg += fmt.Sprintf("%v:<%v,%v,%v,%v>;", rgName, pair.firstTs, firstRU, pair.lastTs, lastRU)
	}
	for rgName, pair := range record {
		if pair.lastRU == nil || pair.firstRU == nil {
			continue
		}
		curRUUsage := *pair.lastRU - *pair.firstRU
		if math.Signbit(curRUUsage) {
			logger.Error("got negative ru usage when compute highest usage tenant",
				zap.Any("first", pair.firstRU), zap.Any("last", pair.lastRU),
				zap.Any("tenant rg", rgName))
			return ""
		}
		if curRUUsage > maxRUUsage {
			maxRUUsage = curRUUsage
			maxRURGName = rgName
		}
	}
	logger.Info("GetHighestRUUsageTenant info", zap.Any("msg", msg), zap.Any("res", maxRURGName), zap.Any("max", maxRUUsage))
	return maxRURGName
}
