package layoutroutingidentitymappingv1

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

// CanonicalizeProposal returns a defensive copy with independently strictly
// sorted Profile and Role dimensions. It preserves present empty dimensions and
// never normalizes key or local-ID text.
func CanonicalizeProposal(proposal Proposal) (Proposal, error) {
	if err := ValidateProposal(proposal); err != nil {
		return Proposal{}, err
	}
	canonical := proposal
	canonical.Profiles = make([]ProfileMapping, len(proposal.Profiles))
	copy(canonical.Profiles, proposal.Profiles)
	canonical.Roles = make([]RoleMapping, len(proposal.Roles))
	copy(canonical.Roles, proposal.Roles)
	sort.Slice(canonical.Profiles, func(left, right int) bool {
		return profileMappingLess(canonical.Profiles[left], canonical.Profiles[right])
	})
	sort.Slice(canonical.Roles, func(left, right int) bool {
		return roleMappingLess(canonical.Roles[left], canonical.Roles[right])
	})
	return canonical, nil
}

// CanonicalProposalBytes returns the v1 cross-language proposal grammar:
// domain, OrgID, key grammar version, canonical Profile count and mappings,
// then canonical Role count and mappings. Each string and count uses minimal
// unsigned-LEB128 byte length/value followed by raw UTF-8 bytes.
func CanonicalProposalBytes(proposal Proposal) ([]byte, error) {
	canonical, err := CanonicalizeProposal(proposal)
	if err != nil {
		return nil, err
	}
	size := canonicalProposalSize(canonical)
	if size > MaxCanonicalProposalBytes {
		return nil, fmt.Errorf(
			"LayoutRoutingIdentityMappingProposal canonical bytes exceed %d",
			MaxCanonicalProposalBytes,
		)
	}
	var out bytes.Buffer
	out.Grow(int(size))
	writeString(&out, CanonicalizationVersion)
	writeString(&out, canonical.OrgID)
	writeString(&out, canonical.KeyGrammarVersion)
	writeUint(&out, uint64(len(canonical.Profiles)))
	for _, mapping := range canonical.Profiles {
		writeString(&out, mapping.Key.NamespaceID)
		writeString(&out, mapping.Key.LocalKey)
		writeString(&out, mapping.ResolvedProfileID)
	}
	writeUint(&out, uint64(len(canonical.Roles)))
	for _, mapping := range canonical.Roles {
		writeString(&out, mapping.Key.NamespaceID)
		writeString(&out, mapping.Key.LocalKey)
		writeString(&out, mapping.ResolvedRoleID)
	}
	return out.Bytes(), nil
}

func profileMappingLess(left, right ProfileMapping) bool {
	return left.Key.NamespaceID < right.Key.NamespaceID ||
		(left.Key.NamespaceID == right.Key.NamespaceID && left.Key.LocalKey < right.Key.LocalKey)
}

func roleMappingLess(left, right RoleMapping) bool {
	return left.Key.NamespaceID < right.Key.NamespaceID ||
		(left.Key.NamespaceID == right.Key.NamespaceID && left.Key.LocalKey < right.Key.LocalKey)
}

func canonicalProposalSize(proposal Proposal) uint64 {
	size := stringSize(CanonicalizationVersion) +
		stringSize(proposal.OrgID) +
		stringSize(proposal.KeyGrammarVersion) +
		uintSize(uint64(len(proposal.Profiles)))
	for _, mapping := range proposal.Profiles {
		size += stringSize(mapping.Key.NamespaceID) +
			stringSize(mapping.Key.LocalKey) +
			stringSize(mapping.ResolvedProfileID)
	}
	size += uintSize(uint64(len(proposal.Roles)))
	for _, mapping := range proposal.Roles {
		size += stringSize(mapping.Key.NamespaceID) +
			stringSize(mapping.Key.LocalKey) +
			stringSize(mapping.ResolvedRoleID)
	}
	return size
}

func stringSize(value string) uint64 {
	return uintSize(uint64(len(value))) + uint64(len(value))
}

func uintSize(value uint64) uint64 {
	var encoded [binary.MaxVarintLen64]byte
	return uint64(binary.PutUvarint(encoded[:], value))
}

func writeString(out *bytes.Buffer, value string) {
	writeUint(out, uint64(len(value)))
	out.WriteString(value)
}

func writeUint(out *bytes.Buffer, value uint64) {
	var encoded [binary.MaxVarintLen64]byte
	count := binary.PutUvarint(encoded[:], value)
	out.Write(encoded[:count])
}
