package layoutroutingidentitymappingv1

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

// ParseCanonicalProposal decodes one exact v1 canonical identity-mapping
// proposal. It rejects truncated, alternate, unordered, or otherwise
// noncanonical byte streams. The returned Proposal owns its own strings and
// slices.
func ParseCanonicalProposal(raw []byte) (Proposal, error) {
	if len(raw) == 0 || len(raw) > MaxCanonicalProposalBytes {
		return Proposal{}, fmt.Errorf(
			"LayoutRoutingIdentityMappingProposal canonical bytes must be 1..%d bytes",
			MaxCanonicalProposalBytes,
		)
	}

	decoder := proposalDecoder{raw: raw}
	domain, err := decoder.readString("domain")
	if err != nil {
		return Proposal{}, err
	}
	if domain != CanonicalizationVersion {
		return Proposal{}, fmt.Errorf(
			"LayoutRoutingIdentityMappingProposal canonical domain = %q, want %q",
			domain,
			CanonicalizationVersion,
		)
	}

	proposal := Proposal{CanonicalizationVersion: domain}
	if proposal.OrgID, err = decoder.readString("OrgID"); err != nil {
		return Proposal{}, err
	}
	if proposal.KeyGrammarVersion, err = decoder.readString("KeyGrammarVersion"); err != nil {
		return Proposal{}, err
	}
	profileCount, err := decoder.readCount("Profile count", MaxProfileMappings)
	if err != nil {
		return Proposal{}, err
	}
	proposal.Profiles = make([]ProfileMapping, int(profileCount))
	for index := range proposal.Profiles {
		mapping, err := decoder.readProfile(index)
		if err != nil {
			return Proposal{}, err
		}
		proposal.Profiles[index] = mapping
	}
	roleCount, err := decoder.readCount("Role count", MaxRoleMappings)
	if err != nil {
		return Proposal{}, err
	}
	proposal.Roles = make([]RoleMapping, int(roleCount))
	for index := range proposal.Roles {
		mapping, err := decoder.readRole(index)
		if err != nil {
			return Proposal{}, err
		}
		proposal.Roles[index] = mapping
	}
	if decoder.offset != len(raw) {
		return Proposal{}, fmt.Errorf(
			"LayoutRoutingIdentityMappingProposal canonical bytes contain %d trailing bytes",
			len(raw)-decoder.offset,
		)
	}
	if err := ValidateProposal(proposal); err != nil {
		return Proposal{}, err
	}
	canonical, err := CanonicalProposalBytes(proposal)
	if err != nil {
		return Proposal{}, err
	}
	if !bytes.Equal(canonical, raw) {
		return Proposal{}, fmt.Errorf("LayoutRoutingIdentityMappingProposal canonical bytes are not exact")
	}
	return proposal, nil
}

type proposalDecoder struct {
	raw    []byte
	offset int
}

func (decoder *proposalDecoder) readProfile(index int) (ProfileMapping, error) {
	label := fmt.Sprintf("Profiles[%d]", index)
	var mapping ProfileMapping
	var err error
	if mapping.Key.NamespaceID, err = decoder.readString(label + ".ProfileKey.NamespaceID"); err != nil {
		return ProfileMapping{}, err
	}
	if mapping.Key.LocalKey, err = decoder.readString(label + ".ProfileKey.LocalKey"); err != nil {
		return ProfileMapping{}, err
	}
	mapping.ResolvedProfileID, err = decoder.readString(label + ".ResolvedProfileID")
	return mapping, err
}

func (decoder *proposalDecoder) readRole(index int) (RoleMapping, error) {
	label := fmt.Sprintf("Roles[%d]", index)
	var mapping RoleMapping
	var err error
	if mapping.Key.NamespaceID, err = decoder.readString(label + ".RoleKey.NamespaceID"); err != nil {
		return RoleMapping{}, err
	}
	if mapping.Key.LocalKey, err = decoder.readString(label + ".RoleKey.LocalKey"); err != nil {
		return RoleMapping{}, err
	}
	mapping.ResolvedRoleID, err = decoder.readString(label + ".ResolvedRoleID")
	return mapping, err
}

func (decoder *proposalDecoder) readCount(label string, maximum int) (uint64, error) {
	value, err := decoder.readUint(label)
	if err != nil {
		return 0, err
	}
	if value > uint64(maximum) {
		return 0, fmt.Errorf(
			"LayoutRoutingIdentityMappingProposal %s = %d, exceeds %d",
			label,
			value,
			maximum,
		)
	}
	return value, nil
}

func (decoder *proposalDecoder) readString(label string) (string, error) {
	length, err := decoder.readUint(label + " length")
	if err != nil {
		return "", err
	}
	remaining := len(decoder.raw) - decoder.offset
	if length > uint64(remaining) {
		return "", fmt.Errorf(
			"LayoutRoutingIdentityMappingProposal %s length %d exceeds remaining bytes %d",
			label,
			length,
			remaining,
		)
	}
	end := decoder.offset + int(length)
	value := string(decoder.raw[decoder.offset:end])
	decoder.offset = end
	return value, nil
}

func (decoder *proposalDecoder) readUint(label string) (uint64, error) {
	if decoder.offset == len(decoder.raw) {
		return 0, fmt.Errorf("LayoutRoutingIdentityMappingProposal %s is truncated", label)
	}
	value, count := binary.Uvarint(decoder.raw[decoder.offset:])
	switch {
	case count == 0:
		return 0, fmt.Errorf("LayoutRoutingIdentityMappingProposal %s is truncated", label)
	case count < 0:
		return 0, fmt.Errorf("LayoutRoutingIdentityMappingProposal %s overflows unsigned LEB128", label)
	case count != int(uintSize(value)):
		return 0, fmt.Errorf("LayoutRoutingIdentityMappingProposal %s uses nonminimal unsigned LEB128", label)
	}
	decoder.offset += count
	return value, nil
}
