package payload

import (
	"bytes"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"math"

	"google.golang.org/protobuf/proto"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)

const keyPrefix = "a="
const maxPayloadSize = 4 * 1024 * 1024

// NewBytes generates a new payload and returns the encoded representation of
// the payload as a slice of bytes. NewBytes uses the fields on the Options
// to create the payload.
func NewBytes(p *Payload) ([]byte, error) {
	p.Padding = make([]byte, 1)
	if p.Time == nil {
		p.Time = timestamppb.Now()
	}
	us, err := CalculateUnpaddedSize(p)
	if err != nil {
		return nil, err
	}
	if p.Size > maxPayloadSize {
		return nil, fmt.Errorf("configured size %d is too large (>%d)", p.Size, maxPayloadSize)
	}
	pSize := int(p.Size) // #nosec -- The "if" above makes this cast safe
	if pSize < us {
		return nil, fmt.Errorf("configured size %d not large enough to fit unpadded transaction of size %d", pSize, us)
	}

	// We halve the padding size because we transform the TX to hex
	p.Padding = make([]byte, (pSize-us)/2)
	_, err = rand.Read(p.Padding)
	if err != nil {
		return nil, err
	}
	b, err := proto.Marshal(p)
	if err != nil {
		return nil, err
	}
	h := []byte(hex.EncodeToString(b))

	// prepend a single key so that the kv store only ever stores a single
	// transaction instead of storing all tx and ballooning in size.
	return append([]byte(keyPrefix), h...), nil
}

// FromBytes extracts a paylod from the byte representation of the payload.
// FromBytes leaves the padding untouched, returning it to the caller to handle
// or discard per their preference.
func FromBytes(b []byte) (*Payload, error) {
	trH := bytes.TrimPrefix(b, []byte(keyPrefix))
	if bytes.Equal(b, trH) {
		return nil, fmt.Errorf("payload bytes missing key prefix '%s'", keyPrefix)
	}
	trB, err := hex.DecodeString(string(trH))
	if err != nil {
		return nil, err
	}

	p := &Payload{}
	err = proto.Unmarshal(trB, p)
	if err != nil {
		return nil, err
	}
	return p, nil
}

// MaxUnpaddedSize returns the maximum size that a payload may be if no padding
// is included.
func MaxUnpaddedSize() (int, error) {
	p := &Payload{
		Time:        timestamppb.Now(),
		Connections: math.MaxUint64,
		Rate:        math.MaxUint64,
		Size:        math.MaxUint64,
		Padding:     make([]byte, 1),
	}
	return CalculateUnpaddedSize(p)
}

// CalculateUnpaddedSize calculates the size of the passed in payload for the
// purpose of determining how much padding to add to add to reach the target size.
// CalculateUnpaddedSize returns an error if the payload Padding field is longer than 1.
func CalculateUnpaddedSize(p *Payload) (int, error) {
	if len(p.Padding) != 1 {
		return 0, fmt.Errorf("expected length of padding to be 1, received %d", len(p.Padding))
	}
	b, err := proto.Marshal(p)
	if err != nil {
		return 0, err
	}
	h := []byte(hex.EncodeToString(b))
	return len(h) + len(keyPrefix), nil
}
