package client

import (
	"fmt"
	"net"

	bin "gordenko.dev/dima/bin/little"
	"gordenko.dev/dima/qb"
	"gordenko.dev/dima/qb/bufreader"
	"gordenko.dev/dima/qb/proto"
)

const (
	metricKeySize = 4
)

type Error struct {
	Code    uint16
	Message string
}

func (s Error) Error() string {
	return fmt.Sprintf("%d: %s", s.Code, s.Message)
}

type Connection struct {
	conn net.Conn
	src  *bufreader.BufferedReader
}

func Connect(address string) (*Connection, error) {
	conn, err := net.Dial("tcp", address)
	if err != nil {
		return nil, err
	}
	return &Connection{
		conn: conn,
		src:  bufreader.New(conn, 1500),
	}, nil
}

func (s *Connection) String() string {
	return s.conn.LocalAddr().String()
}

func (s *Connection) Close() {
	s.conn.Close()
}

func (s *Connection) mustSuccess(reader *bufreader.BufferedReader) (err error) {
	code, err := reader.ReadByte()
	if err != nil {
		return
	}
	switch code {
	case proto.RespSuccess:
		fmt.Println("SUCCESS")
		return nil // ok
	case proto.RespError:
		return s.onError()
	default:
		return fmt.Errorf("unknown reponse code %d", code)
	}
}

func (s *Connection) AddMetric(req proto.AddMetricReq) (err error) {
	arr := []byte{
		proto.TypeAddMetric,
		0, 0, 0, 0, //
		byte(req.MetricType),
		req.FracDigits,
	}
	bin.PutUint32(arr[1:], req.MetricID)
	fmt.Println("arr", arr)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	return s.mustSuccess(s.src)
}

//
// func (s *Connection) UpdateMetric(req Metric) error {
// 	arr := []byte{
// 		proto.TypeUpdateMetric,
// 		0, 0, 0, 0, //
// 		req.FracDigits,
// 	}
// 	bin.PutUint32(arr[1:], req.MetricID)

// 	if _, err := s.conn.Write(arr); err != nil {
// 		return err
// 	}
// 	return s.mustSuccess(s.src)
// }

func (s *Connection) GetMetric(metricID uint32) (_ *proto.Metric, err error) {
	arr := []byte{
		proto.TypeGetMetric,
		0, 0, 0, 0,
	}
	bin.PutUint32(arr[1:], metricID)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	code, err := s.src.ReadByte()
	if err != nil {
		return
	}
	switch code {
	case proto.RespValue:
		var (
			metric     proto.Metric
			metricType byte
		)
		metric.MetricID, err = bin.ReadUint32(s.src)
		if err != nil {
			return
		}
		metricType, err = bin.ReadByte(s.src)
		if err != nil {
			return
		}
		metric.MetricType = qb.MetricType(metricType)
		metric.FracDigits, err = bin.ReadByteAsInt(s.src)
		if err != nil {
			return
		}
		return &metric, nil
	case proto.RespError:
		return nil, s.onMaybeError()
	default:
		return nil, fmt.Errorf("unknown reponse code %d", code)
	}
}

func (s *Connection) DeleteMetric(metricID uint32) (err error) {
	arr := []byte{
		proto.TypeDeleteMetric,
		0, 0, 0, 0, //
	}
	bin.PutUint32(arr[1:], metricID)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	return s.mustSuccess(s.src)
}

// func (s *Connection) AppendMeasure(req proto.AppendMeasureReq) (err error) {
// 	arr := []byte{
// 		proto.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:], req.Timestamp)
// 	bin.PutFloat64(arr[9:], req.Value)
// 	// req
// 	if _, err = s.conn.Write(arr); err != nil {
// 		return
// 	}
// 	return s.mustSuccess(s.src)
// }

type AppendMeasuresResult struct {
	WrittenCount int
	ErrorCode    byte
}

func (s *Connection) AppendMeasures(req proto.AppendMeasuresReq) (result AppendMeasuresResult, err error) {
	if len(req.Measures) > 65535 {
		err = fmt.Errorf("wrong measures qty: %d", len(req.Measures))
		return
	}
	var (
		prefixSize = 7
		recordSize = 12
		arr        = make([]byte, prefixSize+len(req.Measures)*recordSize)
	)
	arr[0] = proto.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
	}
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	code, err := s.src.ReadByte()
	if err != nil {
		return
	}
	switch code {
	case proto.RespValue:
		result.WrittenCount, err = bin.ReadUint16AsInt(s.src)
		if err != nil {
			return
		}
		result.ErrorCode, err = bin.ReadByte(s.src)
		if err != nil {
			return
		}
	case proto.RespError:
		err = s.onMaybeError()
		return
	default:
		err = fmt.Errorf("unknown reponse code %d", code)
		return
	}
	return
}

// type AppendMeasurePerMetricReq struct {
// 	MetricID uint32
// 	Measures []Measure
// }

// func (s *Connection) AppendMeasurePerMetric(list []proto.MetricMeasure) (_ []proto.AppendError, err error) {
// 	if len(list) > 65535 {
// 		return nil, fmt.Errorf("wrong measures qty: %d", len(list))
// 	}
// 	var (
// 		// 3 bytes: 1b message type + 2b records qty
// 		fixedSize  = 3
// 		recordSize = 16
// 		arr        = make([]byte, fixedSize+len(list)*recordSize)
// 	)
// 	arr[0] = proto.TypeAppendMeasures
// 	bin.PutUint16(arr[1:], uint16(len(list)))
// 	pos := fixedSize
// 	for _, item := range list {
// 		bin.PutUint32(arr[pos:], item.MetricID)
// 		bin.PutUint32(arr[pos+4:], item.Timestamp)
// 		bin.PutFloat64(arr[pos+8:], item.Value)
// 		pos += recordSize
// 	}
// 	// req
// 	if _, err = s.conn.Write(arr); err != nil {
// 		return
// 	}
// 	// answer
// 	code, err := s.src.ReadByte()
// 	if err != nil {
// 		return
// 	}
// 	switch code {
// 	case proto.RespValue:
// 		var (
// 			count        int
// 			appendErrors []proto.AppendError
// 		)
// 		count, err = bin.ReadUint16AsInt(s.src)
// 		if err != nil {
// 			return
// 		}
// 		for range count {
// 			var ae proto.AppendError
// 			ae.MetricID, err = bin.ReadUint32(s.src)
// 			if err != nil {
// 				return
// 			}
// 			ae.ErrorCode, err = bin.ReadUint16(s.src)
// 			if err != nil {
// 				return
// 			}
// 			appendErrors = append(appendErrors, ae)
// 		}
// 		return appendErrors, nil
// 	default:
// 		return nil, fmt.Errorf("unknown reponse code %d", code)
// 	}
// }

func (s *Connection) ListAllInstantMeasures(metricID uint32) (_ []proto.InstantMeasure, err error) {
	arr := []byte{
		proto.TypeListAllInstantMeasures,
		0, 0, 0, 0, // metricID
	}
	bin.PutUint32(arr[1:], metricID)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	return s.readInstantMeasures()
}

func (s *Connection) ListInstantMeasures(req proto.ListInstantMeasuresReq) (_ []proto.InstantMeasure, err error) {
	arr := []byte{
		proto.TypeListInstantMeasures,
		0, 0, 0, 0, // metricID
		0, 0, 0, 0, // since
		0, 0, 0, 0, // until
	}
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint32(arr[5:], req.Since)
	bin.PutUint32(arr[9:], req.Until)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	return s.readInstantMeasures()
}

func (s *Connection) readInstantMeasures() (_ []proto.InstantMeasure, err error) {
	var result []proto.InstantMeasure
	for {
		var code byte
		code, err = bin.ReadByte(s.src)
		if err != nil {
			return nil, fmt.Errorf("read response code: %s", err)
		}
		switch code {
		case proto.RespPartOfValue:
			var count int
			count, err = bin.ReadUint32AsInt(s.src)
			if err != nil {
				return nil, fmt.Errorf("read records qty: %s", err)
			}
			for range count {
				var measure proto.InstantMeasure
				measure.Timestamp, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				measure.Value, err = bin.ReadFloat64(s.src)
				if err != nil {
					return
				}
				result = append(result, measure)
			}
		case proto.RespEndOfValue:
			return result, nil
		case proto.RespError:
			return nil, s.onError()
		default:
			return nil, fmt.Errorf("unknown reponse code %d", code)
		}
	}
}

func (s *Connection) ListAllCumulativeMeasures(metricID uint32) (_ []proto.CumulativeMeasure, err error) {
	arr := []byte{
		proto.TypeListAllCumulativeMeasures,
		0, 0, 0, 0, // metricID
	}
	bin.PutUint32(arr[1:], metricID)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	return s.readCumulativeMeasures()
}

func (s *Connection) ListCumulativeMeasures(req proto.ListCumulativeMeasuresReq) (_ []proto.CumulativeMeasure, err error) {
	arr := []byte{
		proto.TypeListCumulativeMeasures,
		0, 0, 0, 0, // metricID
		0, 0, 0, 0, // since
		0, 0, 0, 0, // until
	}
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint32(arr[5:], req.Since)
	bin.PutUint32(arr[9:], req.Until)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	return s.readCumulativeMeasures()
}

func (s *Connection) readCumulativeMeasures() (_ []proto.CumulativeMeasure, err error) {
	var result []proto.CumulativeMeasure
	for {
		var code byte
		code, err = bin.ReadByte(s.src)
		if err != nil {
			return nil, fmt.Errorf("read response code: %s", err)
		}
		fmt.Println("code", code)
		switch code {
		case proto.RespPartOfValue:
			fmt.Println("RespPartOfValue")
			var count int
			count, err = bin.ReadUint32AsInt(s.src)
			if err != nil {
				return nil, fmt.Errorf("read records qty: %s", err)
			}
			for range count {
				var measure proto.CumulativeMeasure
				measure.Timestamp, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				measure.Value, err = bin.ReadFloat64(s.src)
				if err != nil {
					return
				}
				measure.Total, err = bin.ReadFloat64(s.src)
				if err != nil {
					return
				}
				result = append(result, measure)
			}
		case proto.RespEndOfValue:
			fmt.Println("RespEndOfValue")
			return result, nil
		case proto.RespError:
			fmt.Println("RespError")
			return nil, s.onError()
		default:
			return nil, fmt.Errorf("unknown reponse code %d", code)
		}
	}
}

func (s *Connection) ListInstantPeriods(req proto.ListInstantPeriodsReq) (_ []proto.InstantPeriod, err error) {
	arr := []byte{
		proto.TypeListInstantPeriods,
		0, 0, 0, 0, // metricID
		0, 0, 0, 0, // since
		0, 0, 0, 0, // until
		byte(req.GroupBy),
		req.AggregateFuncs,
		byte(req.FirstHourOfDay),
	}
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint16(arr[5:], uint16(req.Since.Year))
	arr[7] = byte(req.Since.Month)
	arr[8] = byte(req.Since.Day)
	bin.PutUint16(arr[9:], uint16(req.Until.Year))
	arr[11] = byte(req.Until.Month)
	arr[12] = byte(req.Until.Day)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	var result []proto.InstantPeriod
	for {
		var code byte
		code, err = s.src.ReadByte()
		if err != nil {
			return
		}
		switch code {
		case proto.RespPartOfValue:
			var count int
			count, err = bin.ReadUint32AsInt(s.src)
			if err != nil {
				return
			}
			for range count {
				var p proto.InstantPeriod
				p.Period, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				p.Since, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				p.Until, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				if (req.AggregateFuncs & qb.AggregateMin) == qb.AggregateMin {
					p.Min, err = bin.ReadFloat64(s.src)
					if err != nil {
						return
					}
				}
				if (req.AggregateFuncs & qb.AggregateMax) == qb.AggregateMax {
					p.Max, err = bin.ReadFloat64(s.src)
					if err != nil {
						return
					}
				}
				if (req.AggregateFuncs & qb.AggregateAvg) == qb.AggregateAvg {
					p.Avg, err = bin.ReadFloat64(s.src)
					if err != nil {
						return
					}
				}
				result = append(result, p)
			}
		case proto.RespEndOfValue:
			return result, nil
		case proto.RespError:
			return nil, s.onError()
		default:
			return nil, fmt.Errorf("unknown reponse code %d", code)
		}
	}
}

func (s *Connection) ListCumulativePeriods(req proto.ListCumulativePeriodsReq) (_ []proto.CumulativePeriod, err error) {
	arr := []byte{
		proto.TypeListCumulativePeriods,
		0, 0, 0, 0, // metricID
		0, 0, 0, 0, // since
		0, 0, 0, 0, // until
		byte(req.GroupBy),
		byte(req.FirstHourOfDay),
	}
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint16(arr[5:], uint16(req.Since.Year))
	arr[7] = byte(req.Since.Month)
	arr[8] = byte(req.Since.Day)
	bin.PutUint16(arr[9:], uint16(req.Until.Year))
	arr[11] = byte(req.Until.Month)
	arr[12] = byte(req.Until.Day)
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	var result []proto.CumulativePeriod
	for {
		var code byte
		code, err = s.src.ReadByte()
		if err != nil {
			return
		}
		switch code {
		case proto.RespPartOfValue:
			var count int
			count, err = bin.ReadUint32AsInt(s.src)
			if err != nil {
				return
			}
			for range count {
				var p proto.CumulativePeriod
				p.Period, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				p.Since, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				p.Until, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				p.EndValue, err = bin.ReadFloat64(s.src)
				if err != nil {
					return
				}
				p.Total, err = bin.ReadFloat64(s.src)
				if err != nil {
					return
				}
				result = append(result, p)
			}
		case proto.RespEndOfValue:
			return result, nil
		case proto.RespError:
			return nil, s.onError()
		default:
			return nil, fmt.Errorf("unknown reponse code %d", code)
		}
	}
}

func (s *Connection) ListCurrentValues(metricIDs []uint32) (_ []proto.CurrentValue, err error) {
	arr := make([]byte, 3+metricKeySize*len(metricIDs))
	arr[0] = proto.TypeListCurrentValues
	bin.PutUint16(arr[1:], uint16(len(metricIDs)))
	off := 3
	for _, metricID := range metricIDs {
		bin.PutUint32(arr[off:], metricID)
		off += metricKeySize
	}
	// req
	if _, err = s.conn.Write(arr); err != nil {
		return
	}
	// answer
	var result []proto.CurrentValue
	for {
		var code byte
		code, err = s.src.ReadByte()
		if err != nil {
			return
		}
		switch code {
		case proto.RespPartOfValue:
			var count int
			count, err = bin.ReadUint32AsInt(s.src)
			if err != nil {
				return
			}
			for range count {
				var m proto.CurrentValue
				m.MetricID, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				m.Timestamp, err = bin.ReadUint32(s.src)
				if err != nil {
					return
				}
				m.Value, err = bin.ReadFloat64(s.src)
				if err != nil {
					return
				}
				result = append(result, m)
			}
		case proto.RespEndOfValue:
			return result, nil
		case proto.RespError:
			return nil, s.onError()
		default:
			return nil, fmt.Errorf("unknown reponse code %d", code)
		}
	}
}

func (s *Connection) DeleteMeasures(req proto.DeleteMeasuresReq) (err error) {
	arr := []byte{
		proto.TypeDeleteMeasures,
		0, 0, 0, 0, // metricID
		0, 0, 0, 0, // since
	}
	bin.PutUint32(arr[1:], req.MetricID)
	bin.PutUint32(arr[5:], req.Since)
	// req
	if _, err := s.conn.Write(arr); err != nil {
		return err
	}
	return s.mustSuccess(s.src)
}

func (s *Connection) onError() error {
	errorCode, err := bin.ReadUint16(s.src)
	if err != nil {
		return fmt.Errorf("read error code: %s", err)
	}
	return Error{
		Code:    errorCode,
		Message: proto.ErrorCodeToText(errorCode),
	}
}

func (s *Connection) onMaybeError() error {
	errorCode, err := bin.ReadUint16(s.src)
	if err != nil {
		return fmt.Errorf("read error code: %s", err)
	}
	if errorCode == proto.ErrNoMetric {
		return nil
	}
	return Error{
		Code:    errorCode,
		Message: proto.ErrorCodeToText(errorCode),
	}
}
