package catalogassignmentidentityv1

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

// CanonicalizeView returns defensive identity slices in v1 stable-key order.
func CanonicalizeView(view View) (View, error) {
	if err := ValidateView(view); err != nil {
		return View{}, err
	}
	canonical := view
	canonical.Objects = append([]ObjectIdentity{}, view.Objects...)
	canonical.RecordTypes = append([]RecordTypeIdentity{}, view.RecordTypes...)
	canonical.ProductApps = append([]ProductAppIdentity{}, view.ProductApps...)
	canonical.NavigationApps = append([]NavigationAppIdentity{}, view.NavigationApps...)
	sort.Slice(canonical.Objects, func(left, right int) bool {
		return canonical.Objects[left].Key.NormalizedAPIName < canonical.Objects[right].Key.NormalizedAPIName
	})
	sort.Slice(canonical.RecordTypes, func(left, right int) bool {
		return recordTypeLess(canonical.RecordTypes[left], canonical.RecordTypes[right])
	})
	sort.Slice(canonical.ProductApps, func(left, right int) bool {
		return canonical.ProductApps[left].Key.AppID < canonical.ProductApps[right].Key.AppID
	})
	sort.Slice(canonical.NavigationApps, func(left, right int) bool {
		return namespacedKeyLess(
			canonical.NavigationApps[left].Key.NamespaceID, canonical.NavigationApps[left].Key.LocalKey,
			canonical.NavigationApps[right].Key.NamespaceID, canonical.NavigationApps[right].Key.LocalKey,
		)
	})
	return canonical, nil
}

// CanonicalViewBytes returns the exact v1 Catalog assignment identity bytes.
func CanonicalViewBytes(view View) ([]byte, error) {
	canonical, err := CanonicalizeView(view)
	if err != nil {
		return nil, err
	}
	size, ok := canonicalViewSize(canonical)
	if !ok {
		return nil, fmt.Errorf("View canonical byte size overflow")
	}
	if err := validateCanonicalViewSize(canonical, MaxCanonicalViewBytes); err != nil {
		return nil, err
	}
	var out bytes.Buffer
	out.Grow(int(size))
	writeString(&out, canonical.CanonicalizationVersion)
	writeString(&out, canonical.SourceCatalogSnapshot.OrgID)
	writeUint(&out, canonical.SourceCatalogSnapshot.CatalogRevision)
	writeString(&out, canonical.SourceCatalogSnapshot.CatalogSnapshotDigest)
	writeUint(&out, canonical.SourceCatalogSnapshot.SchemaGeneration)
	writeUint(&out, canonical.SourceCatalogSnapshot.EngineGeneration)
	writeString(&out, canonical.KeyGrammarVersion)
	writeString(&out, canonical.ObjectKeyNormalizationVersion)
	writeUint(&out, uint64(len(canonical.Objects)))
	for _, identity := range canonical.Objects {
		writeString(&out, identity.Key.NormalizedAPIName)
		writeString(&out, identity.ResolvedObjectID)
		out.WriteByte(stateByte(identity.State))
	}
	writeUint(&out, uint64(len(canonical.RecordTypes)))
	for _, identity := range canonical.RecordTypes {
		writeString(&out, identity.ObjectKey.NormalizedAPIName)
		writeString(&out, identity.Key.NamespaceID)
		writeString(&out, identity.Key.LocalKey)
		writeString(&out, identity.ResolvedRecordTypeID)
		out.WriteByte(stateByte(identity.State))
	}
	writeUint(&out, uint64(len(canonical.ProductApps)))
	for _, identity := range canonical.ProductApps {
		writeString(&out, identity.Key.AppID)
		out.WriteByte(stateByte(identity.State))
		out.WriteByte(byte(identity.ResolvedTarget.Kind))
		if identity.ResolvedTarget.Kind == ProductAppResolvedTargetKindAppInstallationID {
			writeString(&out, identity.ResolvedTarget.ResolvedAppInstallationID)
		}
	}
	writeUint(&out, uint64(len(canonical.NavigationApps)))
	for _, identity := range canonical.NavigationApps {
		writeString(&out, identity.Key.NamespaceID)
		writeString(&out, identity.Key.LocalKey)
		writeString(&out, identity.ResolvedAppConfigID)
		out.WriteByte(stateByte(identity.State))
	}
	return out.Bytes(), nil
}

func validateCanonicalViewSize(view View, limit uint64) error {
	size, ok := canonicalViewSize(view)
	if !ok || size > limit {
		return fmt.Errorf("View canonical bytes exceed %d", limit)
	}
	return nil
}

func canonicalViewSize(view View) (uint64, bool) {
	var size uint64
	add := func(values ...uint64) bool {
		for _, value := range values {
			next, ok := checkedAddSize(size, value)
			if !ok {
				return false
			}
			size = next
		}
		return true
	}
	if !add(
		stringSize(view.CanonicalizationVersion),
		stringSize(view.SourceCatalogSnapshot.OrgID),
		uintSize(view.SourceCatalogSnapshot.CatalogRevision),
		stringSize(view.SourceCatalogSnapshot.CatalogSnapshotDigest),
		uintSize(view.SourceCatalogSnapshot.SchemaGeneration),
		uintSize(view.SourceCatalogSnapshot.EngineGeneration),
		stringSize(view.KeyGrammarVersion),
		stringSize(view.ObjectKeyNormalizationVersion),
		uintSize(uint64(len(view.Objects))),
	) {
		return 0, false
	}
	for _, identity := range view.Objects {
		if !add(stringSize(identity.Key.NormalizedAPIName), stringSize(identity.ResolvedObjectID), 1) {
			return 0, false
		}
	}
	if !add(uintSize(uint64(len(view.RecordTypes)))) {
		return 0, false
	}
	for _, identity := range view.RecordTypes {
		if !add(
			stringSize(identity.ObjectKey.NormalizedAPIName),
			stringSize(identity.Key.NamespaceID),
			stringSize(identity.Key.LocalKey),
			stringSize(identity.ResolvedRecordTypeID),
			1,
		) {
			return 0, false
		}
	}
	if !add(uintSize(uint64(len(view.ProductApps)))) {
		return 0, false
	}
	for _, identity := range view.ProductApps {
		if !add(stringSize(identity.Key.AppID), 2) {
			return 0, false
		}
		if identity.ResolvedTarget.Kind == ProductAppResolvedTargetKindAppInstallationID {
			if !add(stringSize(identity.ResolvedTarget.ResolvedAppInstallationID)) {
				return 0, false
			}
		}
	}
	if !add(uintSize(uint64(len(view.NavigationApps)))) {
		return 0, false
	}
	for _, identity := range view.NavigationApps {
		if !add(
			stringSize(identity.Key.NamespaceID),
			stringSize(identity.Key.LocalKey),
			stringSize(identity.ResolvedAppConfigID),
			1,
		) {
			return 0, false
		}
	}
	return size, true
}

func checkedAddSize(total, value uint64) (uint64, bool) {
	if value > ^uint64(0)-total {
		return 0, false
	}
	return total + value, true
}

func recordTypeLess(left, right RecordTypeIdentity) bool {
	if left.ObjectKey.NormalizedAPIName != right.ObjectKey.NormalizedAPIName {
		return left.ObjectKey.NormalizedAPIName < right.ObjectKey.NormalizedAPIName
	}
	return namespacedKeyLess(left.Key.NamespaceID, left.Key.LocalKey, right.Key.NamespaceID, right.Key.LocalKey)
}

func namespacedKeyLess(leftNamespace, leftLocal, rightNamespace, rightLocal string) bool {
	return leftNamespace < rightNamespace || (leftNamespace == rightNamespace && leftLocal < rightLocal)
}

func stateByte(state IdentityState) byte {
	if state == IdentityStateActive {
		return 0x01
	}
	if state == IdentityStateTombstone {
		return 0x02
	}
	return 0x00
}

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])
}
