package peerwg

import (
	"bytes"
	"strconv"

	"github.com/google/uuid"
	"golang.org/x/xerrors"
	"inet.af/netaddr"
	"tailscale.com/types/key"
)

const handshakeSeparator byte = '|'

// Handshake is a message received from a wireguard peer, indicating
// it would like to connect.
type Handshake struct {
	// Recipient is the uuid of the agent that the message was intended for.
	Recipient uuid.UUID `json:"recipient"`
	// DiscoPublicKey is the disco public key of the peer.
	DiscoPublicKey key.DiscoPublic `json:"disco"`
	// NodePublicKey is the public key of the peer.
	NodePublicKey key.NodePublic `json:"public"`
	// IPv6 is the IPv6 address of the peer.
	IPv6 netaddr.IP `json:"ipv6"`
}

// HandshakeRecipientHint parses the first part of a serialized
// Handshake to quickly determine if the message is meant for the
// provided recipient.
func HandshakeRecipientHint(agentID []byte, msg []byte) (bool, error) {
	idx := bytes.Index(msg, []byte{handshakeSeparator})
	if idx == -1 {
		return false, xerrors.Errorf("invalid peer message, no separator")
	}

	return bytes.Equal(agentID, msg[:idx]), nil
}

func (h *Handshake) UnmarshalText(text []byte) error {
	sp := bytes.Split(text, []byte{handshakeSeparator})
	if len(sp) != 4 {
		return xerrors.Errorf("expected 4 parts, got %d", len(sp))
	}

	err := h.Recipient.UnmarshalText(sp[0])
	if err != nil {
		return xerrors.Errorf("parse recipient: %w", err)
	}

	err = h.DiscoPublicKey.UnmarshalText(sp[1])
	if err != nil {
		return xerrors.Errorf("parse disco: %w", err)
	}

	err = h.NodePublicKey.UnmarshalText(sp[2])
	if err != nil {
		return xerrors.Errorf("parse public: %w", err)
	}

	h.IPv6, err = netaddr.ParseIP(string(sp[3]))
	if err != nil {
		return xerrors.Errorf("parse ipv6: %w", err)
	}

	return nil
}

func (h Handshake) MarshalText() ([]byte, error) {
	const expectedLen = 223
	var buf bytes.Buffer
	buf.Grow(expectedLen)

	recp, _ := h.Recipient.MarshalText()
	_, _ = buf.Write(recp)
	_ = buf.WriteByte(handshakeSeparator)

	disco, _ := h.DiscoPublicKey.MarshalText()
	_, _ = buf.Write(disco)
	_ = buf.WriteByte(handshakeSeparator)

	pub, _ := h.NodePublicKey.MarshalText()
	_, _ = buf.Write(pub)
	_ = buf.WriteByte(handshakeSeparator)

	ipv6 := h.IPv6.StringExpanded()
	_, _ = buf.WriteString(ipv6)

	// Ensure we're always allocating exactly enough.
	if buf.Len() != expectedLen {
		panic("buffer length mismatch: want 223, got " + strconv.Itoa(buf.Len()))
	}
	return buf.Bytes(), nil
}
