package proto

import (
	"fmt"

	octopus "gordenko.dev/dima/diploma"
	"gordenko.dev/dima/diploma/bin"
	"gordenko.dev/dima/diploma/bufreader"
)

const (
	TypeDeleteMeasures            byte = 1
	TypeListCurrentValues         byte = 2
	TypeListInstantMeasures       byte = 3
	TypeListCumulativeMeasures    byte = 33
	TypeListInstantPeriods        byte = 4
	TypeListCumulativePeriods     byte = 44
	TypeGetMetric                 byte = 5
	TypeAddMetric                 byte = 6
	TypeListAllInstantMeasures    byte = 8
	TypeListAllCumulativeMeasures byte = 88
	TypeRangeTotal                byte = 9
	TypeAppendMeasure             byte = 10
	TypeAppendMeasures            byte = 11
	TypeDeleteMetric              byte = 12

	RespPartOfValue byte = 255
	RespEndOfValue  byte = 254
	RespError       byte = 253
	RespSuccess     byte = 252
	RespValue       byte = 251

	ErrNoMetric          = 1
	ErrDuplicate         = 2
	ErrWrongMetricType   = 3
	ErrWrongFracDigits   = 4
	ErrExpiredMeasure    = 5
	ErrNonMonotonicValue = 6
	ErrEmptyMetricID     = 7
	ErrInvalidRange      = 8
	ErrUnexpected        = 9
)

func ErrorCodeToText(code uint16) string {
	switch code {
	case ErrNoMetric:
		return "NoMetric"
	case ErrDuplicate:
		return "Duplicate"
	case ErrWrongMetricType:
		return "WrongMetricType"
	case ErrWrongFracDigits:
		return "WrongFracDigits"
	case ErrExpiredMeasure:
		return "ExpiredMeasure"
	case ErrNonMonotonicValue:
		return "NonMonotonicValue"
	case ErrEmptyMetricID:
		return "EmptyMetricID"
	case ErrInvalidRange:
		return "InvalidRange"
	case ErrUnexpected:
		return "Unexpected"
	default:
		return ""
	}
}

type GetMetricReq struct {
	MetricID uint32
}

type ListCurrentValuesReq struct {
	MetricIDs []uint32
}

type AddMetricReq struct {
	MetricID   uint32
	MetricType octopus.MetricType
	FracDigits int
}

type UpdateMetricReq struct {
	MetricID   uint32
	MetricType octopus.MetricType
	FracDigits int
}

type DeleteMetricReq struct {
	MetricID uint32
}

type DeleteMeasuresReq struct {
	MetricID uint32
	Since    uint32 // timestamp (optional)
}

type AppendMeasureReq struct {
	MetricID  uint32
	Timestamp uint32
	Value     float64
}

type ListAllInstantMetricMeasuresReq struct {
	MetricID uint32
}

type ListAllCumulativeMeasuresReq struct {
	MetricID uint32
}

type ListInstantMeasuresReq struct {
	MetricID       uint32
	Since          uint32
	Until          uint32
	FirstHourOfDay int
}

type ListCumulativeMeasuresReq struct {
	MetricID       uint32
	Since          uint32
	Until          uint32
	FirstHourOfDay int
}

type ListInstantPeriodsReq struct {
	MetricID       uint32
	Since          uint32
	Until          uint32
	GroupBy        octopus.GroupBy
	AggregateFuncs byte
	FirstHourOfDay int
	LastDayOfMonth int
}

type ListCumulativePeriodsReq struct {
	MetricID       uint32
	Since          uint32
	Until          uint32
	GroupBy        octopus.GroupBy
	FirstHourOfDay int
	LastDayOfMonth int
}

type Metric struct {
	MetricID   uint32
	MetricType octopus.MetricType
	FracDigits int
}

type RangeTotalReq struct {
	MetricID uint32
	Since    uint32
	Until    uint32
}

func PackAddMetricReq(req AddMetricReq) []byte {
	arr := []byte{
		TypeAddMetric,
		0, 0, 0, 0, //
		byte(req.MetricType),
		byte(req.FracDigits),
	}
	bin.PutUint32(arr[1:], req.MetricID)
	return arr
}

func PackDeleteMetricReq(req DeleteMetricReq) []byte {
	arr := []byte{
		TypeDeleteMetric,
		0, 0, 0, 0, // metricID
	}
	bin.PutUint32(arr[1:], req.MetricID)
	return arr
}

func PackAppendMeasure(req AppendMeasureReq) []byte {
	arr := []byte{
		TypeAppendMeasure,
		0, 0, 0, 0, // metricID
		0, 0, 0, 0, // timestamp
		0, 0, 0, 0, 0, 0, 0, 0, // value
	}
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint32(arr[5:], uint32(req.Timestamp))
	bin.PutFloat64(arr[9:], req.Value)
	return arr
}

func PackDeleteMeasuresReq(req DeleteMeasuresReq) []byte {
	arr := []byte{
		TypeDeleteMeasures,
		0, 0, 0, 0, // metricID
		0, 0, 0, 0, // since
	}
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint32(arr[5:], uint32(req.Since))
	return arr
}

// UNPACK reqs

func UnpackAddMetricReq(arr []byte) (m AddMetricReq) {
	m.MetricID = bin.GetUint32(arr)
	m.MetricType = octopus.MetricType(arr[4])
	m.FracDigits = int(arr[5])
	return
}

func UnpackUpdateMetricReq(arr []byte) (m UpdateMetricReq) {
	m.MetricID = bin.GetUint32(arr)
	m.MetricType = octopus.MetricType(arr[4])
	m.FracDigits = int(arr[5])
	return
}

func UnpackDeleteMetricReq(arr []byte) (m DeleteMetricReq) {
	m.MetricID = bin.GetUint32(arr)
	return
}

func UnpackAppendMeasureReq(arr []byte) (m AppendMeasureReq) {
	m.MetricID = bin.GetUint32(arr)
	m.Timestamp = bin.GetUint32(arr[4:])
	m.Value = bin.GetFloat64(arr[8:])
	return
}

func UnpackDeleteMeasuresReq(arr []byte) (m DeleteMeasuresReq) {
	m.MetricID = bin.GetUint32(arr)
	m.Since = bin.GetUint32(arr[4:])
	return
}

func UnpackListInstantMeasuresReq(arr []byte) (m ListInstantMeasuresReq) {
	m.MetricID = bin.GetUint32(arr[0:])
	m.Since = bin.GetUint32(arr[4:])
	m.Until = bin.GetUint32(arr[8:])
	m.FirstHourOfDay = int(arr[12])
	return
}

func UnpackListCumulativeMeasuresReq(arr []byte) (m ListCumulativeMeasuresReq) {
	m.MetricID = bin.GetUint32(arr)
	m.Since = bin.GetUint32(arr[4:])
	m.Until = bin.GetUint32(arr[8:])
	m.FirstHourOfDay = int(arr[12])
	return
}

func UnpackListInstantPeriodsReq(arr []byte) (m ListInstantPeriodsReq) {
	m.MetricID = bin.GetUint32(arr)
	m.Since = bin.GetUint32(arr[4:])
	m.Until = bin.GetUint32(arr[8:])
	m.GroupBy = octopus.GroupBy(arr[12])
	m.AggregateFuncs = arr[13]
	m.FirstHourOfDay = int(arr[14])
	m.LastDayOfMonth = int(arr[15])
	return
}

func UnpackListCumulativePeriodsReq(arr []byte) (m ListCumulativePeriodsReq) {
	m.MetricID = bin.GetUint32(arr[0:])
	m.Since = bin.GetUint32(arr[4:])
	m.Until = bin.GetUint32(arr[8:])
	m.GroupBy = octopus.GroupBy(arr[12])
	m.FirstHourOfDay = int(arr[13])
	m.LastDayOfMonth = int(arr[14])
	return
}

func UnpackRangeTotalReq(arr []byte) (m RangeTotalReq) {
	m.MetricID = bin.GetUint32(arr)
	m.Since = bin.GetUint32(arr[4:])
	m.Until = bin.GetUint32(arr[8:])
	return
}

// READ reqs

func ReadGetMetricReq(r *bufreader.BufferedReader) (m GetMetricReq, err error) {
	m.MetricID, err = bin.ReadUint32(r)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return
}

func ReadAddMetricReq(r *bufreader.BufferedReader) (m AddMetricReq, err error) {
	arr, err := r.ReadN(6)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackAddMetricReq(arr), nil
}

func ReadUpdateMetricReq(r *bufreader.BufferedReader) (m UpdateMetricReq, err error) {
	arr, err := r.ReadN(6)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackUpdateMetricReq(arr), nil
}

func ReadDeleteMetricReq(r *bufreader.BufferedReader) (m DeleteMetricReq, err error) {
	m.MetricID, err = bin.ReadUint32(r)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return
}

func ReadAppendMeasureReq(r *bufreader.BufferedReader) (m AppendMeasureReq, err error) {
	arr, err := r.ReadN(16)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackAppendMeasureReq(arr), nil
}

func ReadDeleteMeasuresReq(r *bufreader.BufferedReader) (m DeleteMeasuresReq, err error) {
	arr, err := r.ReadN(8)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackDeleteMeasuresReq(arr), nil
}

func ReadListAllInstantMeasuresReq(r *bufreader.BufferedReader) (m ListAllInstantMetricMeasuresReq, err error) {
	m.MetricID, err = bin.ReadUint32(r)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return
}

func ReadListAllCumulativeMeasuresReq(r *bufreader.BufferedReader) (m ListAllCumulativeMeasuresReq, err error) {
	m.MetricID, err = bin.ReadUint32(r)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return
}

func ReadListInstantMeasuresReq(r *bufreader.BufferedReader) (m ListInstantMeasuresReq, err error) {
	arr, err := r.ReadN(13)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackListInstantMeasuresReq(arr), nil
}

func ReadListCumulativeMeasuresReq(r *bufreader.BufferedReader) (m ListCumulativeMeasuresReq, err error) {
	arr, err := r.ReadN(13)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackListCumulativeMeasuresReq(arr), nil
}

func ReadListInstantPeriodsReq(r *bufreader.BufferedReader) (m ListInstantPeriodsReq, err error) {
	arr, err := r.ReadN(16)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackListInstantPeriodsReq(arr), nil
}

func ReadListCumulativePeriodsReq(r *bufreader.BufferedReader) (m ListCumulativePeriodsReq, err error) {
	arr, err := r.ReadN(15)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackListCumulativePeriodsReq(arr), nil
}

func ReadRangeTotalReq(r *bufreader.BufferedReader) (m RangeTotalReq, err error) {
	arr, err := r.ReadN(12)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}
	return UnpackRangeTotalReq(arr), nil
}

func ReadListCurrentValuesReq(r *bufreader.BufferedReader) (m ListCurrentValuesReq, err error) {
	qty, err := bin.ReadUint16(r)
	if err != nil {
		err = fmt.Errorf("read req: %s", err)
		return
	}

	for i := range int(qty) {
		var metricID uint32
		metricID, err = bin.ReadUint32(r)
		if err != nil {
			err = fmt.Errorf("read metricID (#%d): %s", i, err)
			return
		}
		m.MetricIDs = append(m.MetricIDs, metricID)
	}
	return
}

type AppendMeasuresReq struct {
	MetricID uint32
	Measures []Measure
}

type Measure struct {
	Timestamp uint32
	Value     float64
}

func PackAppendMeasures(req AppendMeasuresReq) []byte {
	if len(req.Measures) > 65535 {
		panic(fmt.Errorf("wrong measures qty: %d", len(req.Measures)))
	}
	var (
		prefixSize = 7
		recordSize = 12
		arr        = make([]byte, prefixSize+len(req.Measures)*recordSize)
	)
	arr[0] = TypeAppendMeasures
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint16(arr[5:], uint16(len(req.Measures)))
	pos := prefixSize
	for _, measure := range req.Measures {
		bin.PutUint32(arr[pos:], measure.Timestamp)
		bin.PutFloat64(arr[pos+4:], measure.Value)
		pos += recordSize
	}
	return arr
}

func ReadAppendMeasuresReq(r *bufreader.BufferedReader) (m AppendMeasuresReq, err error) {
	prefix, err := bin.ReadN(r, 6) // metricID + measures qty
	if err != nil {
		err = fmt.Errorf("read prefix: %s", err)
		return
	}

	m.MetricID = bin.GetUint32(prefix[0:])
	qty := bin.GetUint16(prefix[4:])

	for i := range int(qty) {
		var measure Measure
		measure.Timestamp, err = bin.ReadUint32(r)
		if err != nil {
			err = fmt.Errorf("read timestamp (#%d): %s", i, err)
			return
		}
		measure.Value, err = bin.ReadFloat64(r)
		if err != nil {
			err = fmt.Errorf("read value (#%d): %s", i, err)
			return
		}
		m.Measures = append(m.Measures, measure)
	}
	return
}
