package wire

import (
	"bytes"
	"encoding/binary"
	"time"

	uuid "github.com/gofrs/uuid"
	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	"github.com/pkg/errors"

	"code.justin.tv/eventbus/client/internal"
)

const version1Tag = 0x01

var (
	errTooShort       = errors.New("Message too short")
	errVersionUnknown = errors.New("Unknown message version")
)

func MakeHeader(msgid uuid.UUID, eventType, environment string, when time.Time) (*HeaderV1, error) {
	ts, err := ptypes.TimestampProto(when)
	if err != nil {
		return nil, err
	}

	return &HeaderV1{
		MessageId:   msgid.Bytes(),
		EventType:   eventType,
		Environment: environment,
		CreatedAt:   ts,
	}, nil
}

// Encode a payload with default values set.
func DefaultsEncode(payload internal.Message, environment string) ([]byte, error) {
	msgid, err := uuid.NewV4()
	if err != nil {
		return nil, errors.Wrap(err, "could not generate UUID")
	}

	header, err := MakeHeader(msgid, payload.EventBusName(), environment, time.Now())
	if err != nil {
		return nil, err
	}
	return EncodeRaw(header, payload)
}

// Encode a v1 payload.
func EncodeRaw(header *HeaderV1, payload proto.Message) ([]byte, error) {
	payloadBuf, err := proto.Marshal(payload)
	if err != nil {
		return nil, errors.Wrap(err, "Could not encode payload")
	}
	return buildMessage(header, payloadBuf)
}

func buildMessage(header *HeaderV1, payloadBuf []byte) ([]byte, error) {
	headerBuf, err := proto.Marshal(header)
	if err != nil {
		return nil, errors.Wrap(err, "Could not encode header")
	}

	var buf bytes.Buffer
	expectedLength := 3 + len(headerBuf) + len(payloadBuf)
	buf.Grow(expectedLength)
	buf.WriteByte(version1Tag)
	binary.Write(&buf, binary.BigEndian, uint16(len(headerBuf)))
	buf.Write(headerBuf)
	buf.Write(payloadBuf)

	return buf.Bytes(), nil
}

// Taken a message payload, split the header/payload and decode the header.
func HeaderAndPayload(msg []byte) (*HeaderV1, []byte, error) {
	if len(msg) < 3 {
		return nil, nil, errTooShort
	} else if msg[0] != version1Tag {
		return nil, nil, errVersionUnknown
	}
	headerLen := binary.BigEndian.Uint16(msg[1:3])
	dst := &HeaderV1{}
	err := proto.Unmarshal(msg[3:3+headerLen], dst)
	if err != nil {
		return nil, nil, errors.Wrap(err, "Could not decode header")
	}
	return dst, msg[3+headerLen:], nil
}
