package layoutroutingsecurityv1

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

// CanonicalizeView returns a defensive copy with Profile and Role entries
// strictly bytewise sorted by their typed keys. It never normalizes key text.
func CanonicalizeView(view View) (View, error) {
	if err := ValidateView(view); err != nil {
		return View{}, err
	}
	canonical := view
	canonical.Profiles = append([]ProfileIdentity{}, view.Profiles...)
	canonical.Roles = make([]RoleIdentity, len(view.Roles))
	for index, identity := range view.Roles {
		canonical.Roles[index] = identity
		if identity.ParentRoleKey != nil {
			parent := *identity.ParentRoleKey
			canonical.Roles[index].ParentRoleKey = &parent
		}
	}
	sort.Slice(canonical.Profiles, func(left, right int) bool {
		return profileKeyLess(canonical.Profiles[left].Key, canonical.Profiles[right].Key)
	})
	sort.Slice(canonical.Roles, func(left, right int) bool {
		return roleKeyLess(canonical.Roles[left].Key, canonical.Roles[right].Key)
	})
	return canonical, nil
}

// CanonicalViewBytes returns the v1 cross-language content grammar:
//
//   - the ASCII ViewCanonicalizationVersion, encoded as a string, as domain;
//   - OrgID, KeyGrammarVersion, and RoleTopologyGrammarVersion;
//   - the Profile count, then key NamespaceID, key LocalKey,
//     ResolvedProfileID, and state for each canonical Profile entry;
//   - the Role count, then key NamespaceID, key LocalKey, ResolvedRoleID,
//     state, parent presence, and optional parent NamespaceID/LocalKey for each
//     canonical Role entry;
//   - strings as minimal unsigned-LEB128 UTF-8 byte length plus raw bytes;
//   - counts as minimal unsigned LEB128 values;
//   - states as 0x01 ACTIVE or 0x02 TOMBSTONE; and
//   - Role parent presence as 0x00 absent or 0x01 present, followed by its key.
//
// SecurityEpoch is intentionally absent so separate epochs may attest the same
// unchanged view content.
func CanonicalViewBytes(view View) ([]byte, error) {
	canonical, err := CanonicalizeView(view)
	if err != nil {
		return nil, err
	}
	size := canonicalViewSize(canonical)
	if size > MaxCanonicalViewBytes {
		return nil, fmt.Errorf("LayoutRoutingSecurityView canonical bytes exceed %d", MaxCanonicalViewBytes)
	}
	var out bytes.Buffer
	out.Grow(int(size))
	writeCanonicalString(&out, ViewCanonicalizationVersion)
	writeCanonicalString(&out, canonical.OrgID)
	writeCanonicalString(&out, canonical.KeyGrammarVersion)
	writeCanonicalString(&out, canonical.RoleTopologyGrammarVersion)
	writeCanonicalUint(&out, uint64(len(canonical.Profiles)))
	for _, identity := range canonical.Profiles {
		writeCanonicalString(&out, identity.Key.NamespaceID)
		writeCanonicalString(&out, identity.Key.LocalKey)
		writeCanonicalString(&out, identity.ResolvedProfileID)
		out.WriteByte(canonicalStateByte(identity.State))
	}
	writeCanonicalUint(&out, uint64(len(canonical.Roles)))
	for _, identity := range canonical.Roles {
		writeCanonicalString(&out, identity.Key.NamespaceID)
		writeCanonicalString(&out, identity.Key.LocalKey)
		writeCanonicalString(&out, identity.ResolvedRoleID)
		out.WriteByte(canonicalStateByte(identity.State))
		if identity.ParentRoleKey == nil {
			out.WriteByte(0)
			continue
		}
		out.WriteByte(1)
		writeCanonicalString(&out, identity.ParentRoleKey.NamespaceID)
		writeCanonicalString(&out, identity.ParentRoleKey.LocalKey)
	}
	return out.Bytes(), nil
}

func profileKeyLess(left, right ProfileKey) bool {
	return left.NamespaceID < right.NamespaceID ||
		(left.NamespaceID == right.NamespaceID && left.LocalKey < right.LocalKey)
}

func roleKeyLess(left, right RoleKey) bool {
	return left.NamespaceID < right.NamespaceID ||
		(left.NamespaceID == right.NamespaceID && left.LocalKey < right.LocalKey)
}

func canonicalStateByte(state IdentityState) byte {
	if state == IdentityStateActive {
		return 0x01
	}
	return 0x02
}

func canonicalViewSize(view View) uint64 {
	size := canonicalStringSize(ViewCanonicalizationVersion) +
		canonicalStringSize(view.OrgID) +
		canonicalStringSize(view.KeyGrammarVersion) +
		canonicalStringSize(view.RoleTopologyGrammarVersion) +
		canonicalUintSize(uint64(len(view.Profiles)))
	for _, identity := range view.Profiles {
		size += canonicalStringSize(identity.Key.NamespaceID) +
			canonicalStringSize(identity.Key.LocalKey) +
			canonicalStringSize(identity.ResolvedProfileID) + 1
	}
	size += canonicalUintSize(uint64(len(view.Roles)))
	for _, identity := range view.Roles {
		size += canonicalStringSize(identity.Key.NamespaceID) +
			canonicalStringSize(identity.Key.LocalKey) +
			canonicalStringSize(identity.ResolvedRoleID) + 2
		if identity.ParentRoleKey != nil {
			size += canonicalStringSize(identity.ParentRoleKey.NamespaceID) +
				canonicalStringSize(identity.ParentRoleKey.LocalKey)
		}
	}
	return size
}

func canonicalStringSize(value string) uint64 {
	return canonicalUintSize(uint64(len(value))) + uint64(len(value))
}

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

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

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