package layoutactivationsetv1

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

	"github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundlekeyv1"
)

// CanonicalizeSet returns a defensive entry slice in canonical BundleKey
// order without mutating caller-owned storage.
func CanonicalizeSet(set Set) (Set, error) {
	if err := ValidateSet(set); err != nil {
		return Set{}, err
	}
	canonical := set
	canonical.Entries = append([]Entry{}, set.Entries...)
	sort.Slice(canonical.Entries, func(left, right int) bool {
		return bundleKeyLess(canonical.Entries[left].BundleKey, canonical.Entries[right].BundleKey)
	})
	return canonical, nil
}

// CanonicalSetBytes returns the exact v1 LayoutActivationSet bytes.
func CanonicalSetBytes(set Set) ([]byte, error) {
	canonical, err := CanonicalizeSet(set)
	if err != nil {
		return nil, err
	}
	size, err := measureCanonicalSet(canonical, MaxCanonicalSetBytes)
	if err != nil {
		return nil, err
	}
	var out bytes.Buffer
	out.Grow(int(size))
	writeCanonicalString(&out, canonical.CanonicalizationVersion)
	writeCanonicalString(&out, canonical.OrgID)
	writeCanonicalUint(&out, canonical.CatalogSchema.CatalogSnapshot.CatalogRevision)
	writeCanonicalString(&out, canonical.CatalogSchema.CatalogSnapshot.CatalogSnapshotDigest)
	writeCanonicalUint(&out, canonical.CatalogSchema.CatalogSnapshot.SchemaGeneration)
	writeCanonicalUint(&out, canonical.CatalogSchema.CatalogSnapshot.EngineGeneration)
	writeCanonicalString(&out, canonical.CatalogSchema.LayoutValidationViewDigest)
	writeCanonicalUint(&out, canonical.RoutingSecurity.SecurityEpoch)
	writeCanonicalString(&out, canonical.RoutingSecurity.LayoutRoutingSecurityViewDigest)
	writeCanonicalString(&out, layoutbundlekeyv1.RequiredBundleKeySetVersion)
	writeCanonicalString(&out, layoutbundlekeyv1.BundleKeyVersion)
	writeCanonicalString(&out, canonical.RequiredBundleKeySetDigest)
	writeCanonicalString(&out, canonical.PreparationProvenanceDigest)
	writeCanonicalUint(&out, uint64(len(canonical.Entries)))
	for index, entry := range canonical.Entries {
		tuple, err := layoutbundlekeyv1.CanonicalBundleKeyTupleBytes(entry.BundleKey)
		if err != nil {
			return nil, fmt.Errorf("canonical entry[%d] BundleKey: %w", index, err)
		}
		out.Write(tuple)
		writeCanonicalString(&out, string(entry.LayoutBundleRevisionID))
		writeCanonicalString(&out, string(entry.LayoutValidationID))
		writeCanonicalString(&out, entry.ResolvedLayoutViewDigest)
	}
	return out.Bytes(), nil
}

func validateCanonicalSetSize(set Set, limit uint64) error {
	_, err := measureCanonicalSet(set, limit)
	return err
}

func canonicalSetSize(set Set) (uint64, error) {
	return measureCanonicalSet(set, ^uint64(0))
}

func measureCanonicalSet(set Set, limit uint64) (uint64, error) {
	measure := canonicalSizeMeasure{limit: limit}
	measure.string(set.CanonicalizationVersion)
	measure.string(set.OrgID)
	measure.uint(set.CatalogSchema.CatalogSnapshot.CatalogRevision)
	measure.string(set.CatalogSchema.CatalogSnapshot.CatalogSnapshotDigest)
	measure.uint(set.CatalogSchema.CatalogSnapshot.SchemaGeneration)
	measure.uint(set.CatalogSchema.CatalogSnapshot.EngineGeneration)
	measure.string(set.CatalogSchema.LayoutValidationViewDigest)
	measure.uint(set.RoutingSecurity.SecurityEpoch)
	measure.string(set.RoutingSecurity.LayoutRoutingSecurityViewDigest)
	measure.string(layoutbundlekeyv1.RequiredBundleKeySetVersion)
	measure.string(layoutbundlekeyv1.BundleKeyVersion)
	measure.string(set.RequiredBundleKeySetDigest)
	measure.string(set.PreparationProvenanceDigest)
	measure.uint(uint64(len(set.Entries)))
	for _, entry := range set.Entries {
		measure.add(1)
		measure.string(entry.BundleKey.SlotOwnerNamespace)
		measure.string(string(entry.BundleKey.LocalComponentKey))
		measure.string(string(entry.LayoutBundleRevisionID))
		measure.string(string(entry.LayoutValidationID))
		measure.string(entry.ResolvedLayoutViewDigest)
		if measure.exceeded {
			return 0, fmt.Errorf("Set canonical bytes exceed %d", limit)
		}
	}
	if measure.exceeded {
		return 0, fmt.Errorf("Set canonical bytes exceed %d", limit)
	}
	return measure.size, nil
}

type canonicalSizeMeasure struct {
	size     uint64
	limit    uint64
	exceeded bool
}

func (measure *canonicalSizeMeasure) add(value uint64) {
	if measure.exceeded {
		return
	}
	if measure.size > measure.limit || value > measure.limit-measure.size {
		measure.exceeded = true
		return
	}
	measure.size += value
}

func (measure *canonicalSizeMeasure) uint(value uint64) {
	measure.add(canonicalUintSize(value))
}

func (measure *canonicalSizeMeasure) string(value string) {
	measure.uint(uint64(len(value)))
	measure.add(uint64(len(value)))
}

func bundleKeyLess(left, right layoutbundlekeyv1.BundleKey) bool {
	leftKind, _ := layoutbundlekeyv1.CanonicalSlotOwnerKindByte(left.SlotOwnerKind)
	rightKind, _ := layoutbundlekeyv1.CanonicalSlotOwnerKindByte(right.SlotOwnerKind)
	if leftKind != rightKind {
		return leftKind < rightKind
	}
	return left.SlotOwnerNamespace < right.SlotOwnerNamespace ||
		(left.SlotOwnerNamespace == right.SlotOwnerNamespace && string(left.LocalComponentKey) < string(right.LocalComponentKey))
}

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