package layoutbundleownerverificationv1

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

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

// CanonicalReceiptBytes returns the complete v1 singular receipt grammar.
func CanonicalReceiptBytes(receipt Receipt) ([]byte, error) {
	if err := ValidateReceipt(receipt); err != nil {
		return nil, err
	}
	keyBytes, err := layoutbundlekeyv1.CanonicalBundleKeyBytes(receipt.BundleKey)
	if err != nil {
		return nil, err
	}
	var out bytes.Buffer
	out.Grow(int(canonicalReceiptSize(receipt)))
	writeCanonicalString(&out, receipt.ReceiptVersion)
	writeCanonicalString(&out, receipt.OrgID)
	writeCanonicalString(&out, receipt.WriterCommandID)
	writeCanonicalUint(&out, receipt.WriterReservationGeneration)
	writeCanonicalString(&out, receipt.SourceRequestDigest)
	writeCanonicalString(&out, receipt.RequiredBundleKeySetDigest)
	out.Write(keyBytes)
	writeCanonicalString(&out, receipt.EvidenceVersion)
	writeCanonicalString(&out, receipt.EvidenceDigest)
	out.WriteByte(byte(receipt.VerificationState))
	return out.Bytes(), nil
}

// CanonicalizeReceiptSet returns a defensive receipt slice in canonical
// BundleKey order after validating exact required-set completeness.
func CanonicalizeReceiptSet(
	set ReceiptSet,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) (ReceiptSet, error) {
	if err := ValidateReceiptSet(set, required); err != nil {
		return ReceiptSet{}, err
	}
	canonical := set
	canonical.Receipts = append([]ReceiptEntry{}, set.Receipts...)
	sort.Slice(canonical.Receipts, func(left, right int) bool {
		return bundleKeyLess(
			canonical.Receipts[left].BundleKey,
			canonical.Receipts[right].BundleKey,
		)
	})
	return canonical, nil
}

// CanonicalReceiptSetBytes returns the complete v1 set grammar. Shared source
// fields and BundleKeyVersion are encoded once; each sorted entry contains only
// the BundleKey tuple and evidence projection.
func CanonicalReceiptSetBytes(
	set ReceiptSet,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) ([]byte, error) {
	canonical, err := CanonicalizeReceiptSet(set, required)
	if err != nil {
		return nil, err
	}
	size, ok := canonicalReceiptSetSize(canonical)
	if !ok || size > MaxCanonicalReceiptSetBytes {
		return nil, fmt.Errorf("ReceiptSet canonical bytes exceed %d", MaxCanonicalReceiptSetBytes)
	}
	var out bytes.Buffer
	out.Grow(int(size))
	writeCanonicalString(&out, canonical.ReceiptSetVersion)
	writeCanonicalString(&out, canonical.ReceiptVersion)
	writeCanonicalString(&out, canonical.OrgID)
	writeCanonicalString(&out, canonical.WriterCommandID)
	writeCanonicalUint(&out, canonical.WriterReservationGeneration)
	writeCanonicalString(&out, canonical.SourceRequestDigest)
	writeCanonicalString(&out, canonical.RequiredBundleKeySetDigest)
	writeCanonicalString(&out, layoutbundlekeyv1.BundleKeyVersion)
	writeCanonicalUint(&out, uint64(len(canonical.Receipts)))
	for _, entry := range canonical.Receipts {
		ownerKind, err := layoutbundlekeyv1.CanonicalSlotOwnerKindByte(entry.BundleKey.SlotOwnerKind)
		if err != nil {
			return nil, err
		}
		out.WriteByte(ownerKind)
		writeCanonicalString(&out, entry.BundleKey.SlotOwnerNamespace)
		writeCanonicalString(&out, string(entry.BundleKey.LocalComponentKey))
		writeCanonicalString(&out, entry.EvidenceVersion)
		writeCanonicalString(&out, entry.EvidenceDigest)
		out.WriteByte(byte(entry.VerificationState))
	}
	return out.Bytes(), nil
}

// CanonicalReceiptDigests reconstructs and hashes every singular receipt in
// canonical BundleKey order. The returned slice is defensive.
func CanonicalReceiptDigests(
	set ReceiptSet,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) ([]string, error) {
	canonical, err := CanonicalizeReceiptSet(set, required)
	if err != nil {
		return nil, err
	}
	digests := make([]string, len(canonical.Receipts))
	for index, entry := range canonical.Receipts {
		digests[index], err = ReceiptDigest(ReconstructReceipt(canonical, entry))
		if err != nil {
			return nil, err
		}
	}
	return digests, nil
}

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

func canonicalReceiptSize(receipt Receipt) uint64 {
	keySize := uint64(len(layoutbundlekeyv1.BundleKeyVersion)) +
		canonicalUintSize(uint64(len(layoutbundlekeyv1.BundleKeyVersion))) +
		1 +
		canonicalStringSize(receipt.BundleKey.SlotOwnerNamespace) +
		canonicalStringSize(string(receipt.BundleKey.LocalComponentKey))
	return canonicalStringSize(receipt.ReceiptVersion) +
		canonicalStringSize(receipt.OrgID) +
		canonicalStringSize(receipt.WriterCommandID) +
		canonicalUintSize(receipt.WriterReservationGeneration) +
		canonicalStringSize(receipt.SourceRequestDigest) +
		canonicalStringSize(receipt.RequiredBundleKeySetDigest) +
		keySize +
		canonicalStringSize(receipt.EvidenceVersion) +
		canonicalStringSize(receipt.EvidenceDigest) +
		1
}

func canonicalReceiptSetSize(set ReceiptSet) (uint64, bool) {
	var size uint64
	add := func(value uint64) bool {
		if value > ^uint64(0)-size {
			return false
		}
		size += value
		return true
	}
	for _, value := range []uint64{
		canonicalStringSize(set.ReceiptSetVersion),
		canonicalStringSize(set.ReceiptVersion),
		canonicalStringSize(set.OrgID),
		canonicalStringSize(set.WriterCommandID),
		canonicalUintSize(set.WriterReservationGeneration),
		canonicalStringSize(set.SourceRequestDigest),
		canonicalStringSize(set.RequiredBundleKeySetDigest),
		canonicalStringSize(layoutbundlekeyv1.BundleKeyVersion),
		canonicalUintSize(uint64(len(set.Receipts))),
	} {
		if !add(value) {
			return 0, false
		}
	}
	for _, entry := range set.Receipts {
		for _, value := range []uint64{
			1,
			canonicalStringSize(entry.BundleKey.SlotOwnerNamespace),
			canonicalStringSize(string(entry.BundleKey.LocalComponentKey)),
			canonicalStringSize(entry.EvidenceVersion),
			canonicalStringSize(entry.EvidenceDigest),
			1,
		} {
			if !add(value) {
				return 0, false
			}
		}
	}
	return size, true
}

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