package catalogpublication

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

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

const (
	// SourceAppSelectionSetVersion versions and domain-separates the complete
	// App activation input selected while compiling one Catalog candidate.
	SourceAppSelectionSetVersion = "nexuscrm.catalog-source-app-selection-set/v1"

	// MaxSourceAppSelectionEntries bounds one complete source selection set.
	MaxSourceAppSelectionEntries = 100_000
	// MaxCanonicalSourceAppSelectionSetBytes bounds the complete canonical
	// source selection representation.
	MaxCanonicalSourceAppSelectionSetBytes = 32 << 20
)

// SourceAppSelectionKind identifies whether source preparation selected the
// exact active App tuple or the exact prospective tuple owned by the writer
// command.
type SourceAppSelectionKind string

const (
	SourceAppSelectionKindActive    SourceAppSelectionKind = "ACTIVE"
	SourceAppSelectionKindCandidate SourceAppSelectionKind = "CANDIDATE"
)

// SourceAppSelection is one exact App activation tuple that contributed to a
// Catalog candidate snapshot. AppLifecycleCandidateCommandID is absent when
// empty and may be present only for a CANDIDATE selection.
type SourceAppSelection struct {
	AppID                          string
	AppInstallationID              string
	AppReleaseDigest               string
	SelectionKind                  SourceAppSelectionKind
	AppLifecycleCandidateCommandID string
}

// SourceAppSelectionSet is the complete App activation input selected while
// compiling one Catalog candidate. Entries must be present even when empty;
// input order is not semantic.
type SourceAppSelectionSet struct {
	SourceAppSelectionSetVersion string
	Entries                      []SourceAppSelection
}

// ValidateSourceAppSelectionSet validates the exact set, the writer-command
// fence for CANDIDATE entries, and resolution of every App-owned required
// BundleKey by owner namespace.
func ValidateSourceAppSelectionSet(
	set SourceAppSelectionSet,
	writerCommandID string,
	requiredSet layoutbundlekeyv1.RequiredBundleKeySet,
) error {
	_, err := CanonicalizeSourceAppSelectionSet(set, writerCommandID, requiredSet)
	return err
}

// CanonicalizeSourceAppSelectionSet returns a defensive entry slice sorted by
// ascending unsigned raw AppID bytes. It does not mutate or retain the caller's
// slice.
func CanonicalizeSourceAppSelectionSet(
	set SourceAppSelectionSet,
	writerCommandID string,
	requiredSet layoutbundlekeyv1.RequiredBundleKeySet,
) (SourceAppSelectionSet, error) {
	if set.SourceAppSelectionSetVersion != SourceAppSelectionSetVersion {
		return SourceAppSelectionSet{}, fmt.Errorf(
			"SourceAppSelectionSet.SourceAppSelectionSetVersion must equal %q",
			SourceAppSelectionSetVersion,
		)
	}
	if set.Entries == nil {
		return SourceAppSelectionSet{}, fmt.Errorf("SourceAppSelectionSet.Entries must be present")
	}
	if err := validateSourceAppSelectionEntryCount(uint64(len(set.Entries))); err != nil {
		return SourceAppSelectionSet{}, err
	}
	if !validCommandID(writerCommandID) {
		return SourceAppSelectionSet{}, fmt.Errorf("SourceAppSelectionSet WriterCommandID must be a lowercase UUIDv7")
	}
	canonicalRequiredSet, err := layoutbundlekeyv1.CanonicalizeRequiredBundleKeySet(requiredSet)
	if err != nil {
		return SourceAppSelectionSet{}, fmt.Errorf("SourceAppSelectionSet RequiredBundleKeySet: %w", err)
	}

	canonical := SourceAppSelectionSet{
		SourceAppSelectionSetVersion: set.SourceAppSelectionSetVersion,
		Entries:                      append([]SourceAppSelection{}, set.Entries...),
	}
	appIDs := make(map[string]struct{}, len(canonical.Entries))
	installationIDs := make(map[string]struct{}, len(canonical.Entries))
	for index, entry := range canonical.Entries {
		if err := validateSourceAppSelection(entry, writerCommandID); err != nil {
			return SourceAppSelectionSet{}, fmt.Errorf("SourceAppSelectionSet.Entries[%d]: %w", index, err)
		}
		if _, duplicate := appIDs[entry.AppID]; duplicate {
			return SourceAppSelectionSet{}, fmt.Errorf("SourceAppSelectionSet.Entries contains duplicate AppID %q", entry.AppID)
		}
		appIDs[entry.AppID] = struct{}{}
		if _, duplicate := installationIDs[entry.AppInstallationID]; duplicate {
			return SourceAppSelectionSet{}, fmt.Errorf(
				"SourceAppSelectionSet.Entries contains duplicate AppInstallationID %q",
				entry.AppInstallationID,
			)
		}
		installationIDs[entry.AppInstallationID] = struct{}{}
	}
	for _, key := range canonicalRequiredSet.BundleKeys {
		if key.SlotOwnerKind != layoutbundlekeyv1.SlotOwnerKindApp {
			continue
		}
		if _, found := appIDs[key.SlotOwnerNamespace]; !found {
			return SourceAppSelectionSet{}, fmt.Errorf(
				"SourceAppSelectionSet has no selection for required App BundleKey namespace %q",
				key.SlotOwnerNamespace,
			)
		}
	}

	sort.Slice(canonical.Entries, func(left, right int) bool {
		return canonical.Entries[left].AppID < canonical.Entries[right].AppID
	})
	if err := validateSourceAppSelectionCanonicalSize(canonicalSourceAppSelectionSetSize(canonical)); err != nil {
		return SourceAppSelectionSet{}, err
	}
	return canonical, nil
}

// CanonicalSourceAppSelectionSetBytes returns the complete v1 grammar:
//
//	S(SourceAppSelectionSetVersion) + U(EntryCount) + for each sorted entry:
//	S(AppID) + S(AppInstallationID) + S(AppReleaseDigest)
//	+ one SelectionKind byte + O(S(AppLifecycleCandidateCommandID)).
//
// The writer command and required set are validation context and are not
// encoded into this value.
func CanonicalSourceAppSelectionSetBytes(
	set SourceAppSelectionSet,
	writerCommandID string,
	requiredSet layoutbundlekeyv1.RequiredBundleKeySet,
) ([]byte, error) {
	canonical, err := CanonicalizeSourceAppSelectionSet(set, writerCommandID, requiredSet)
	if err != nil {
		return nil, err
	}
	size := canonicalSourceAppSelectionSetSize(canonical)
	if err := validateSourceAppSelectionCanonicalSize(size); err != nil {
		return nil, err
	}

	var out bytes.Buffer
	out.Grow(int(size))
	writeSourceAppSelectionString(&out, canonical.SourceAppSelectionSetVersion)
	writeSourceAppSelectionUint(&out, uint64(len(canonical.Entries)))
	for _, entry := range canonical.Entries {
		writeSourceAppSelectionString(&out, entry.AppID)
		writeSourceAppSelectionString(&out, entry.AppInstallationID)
		writeSourceAppSelectionString(&out, entry.AppReleaseDigest)
		out.WriteByte(sourceAppSelectionKindByte(entry.SelectionKind))
		if entry.AppLifecycleCandidateCommandID == "" {
			out.WriteByte(0x00)
			continue
		}
		out.WriteByte(0x01)
		writeSourceAppSelectionString(&out, entry.AppLifecycleCandidateCommandID)
	}
	return out.Bytes(), nil
}

func validateSourceAppSelection(entry SourceAppSelection, writerCommandID string) error {
	if err := appauthority.ValidateAppID(entry.AppID); err != nil {
		return fmt.Errorf("AppID: %w", err)
	}
	if err := appauthority.ValidateAppInstallationID(entry.AppInstallationID); err != nil {
		return fmt.Errorf("AppInstallationID: %w", err)
	}
	if err := appauthority.ValidateDigest(entry.AppReleaseDigest); err != nil {
		return fmt.Errorf("AppReleaseDigest: %w", err)
	}
	switch entry.SelectionKind {
	case SourceAppSelectionKindActive:
		if entry.AppLifecycleCandidateCommandID != "" {
			return fmt.Errorf("ACTIVE selection must not contain AppLifecycleCandidateCommandID")
		}
	case SourceAppSelectionKindCandidate:
		if !validCommandID(entry.AppLifecycleCandidateCommandID) {
			return fmt.Errorf("CANDIDATE AppLifecycleCandidateCommandID must be a lowercase UUIDv7")
		}
		if entry.AppLifecycleCandidateCommandID != writerCommandID {
			return fmt.Errorf("CANDIDATE AppLifecycleCandidateCommandID must equal WriterCommandID")
		}
	default:
		return fmt.Errorf("SelectionKind must be ACTIVE or CANDIDATE")
	}
	return nil
}

func sourceAppSelectionKindByte(kind SourceAppSelectionKind) byte {
	switch kind {
	case SourceAppSelectionKindActive:
		return 0x01
	case SourceAppSelectionKindCandidate:
		return 0x02
	default:
		return 0x00
	}
}

func validateSourceAppSelectionEntryCount(count uint64) error {
	if count > MaxSourceAppSelectionEntries {
		return fmt.Errorf("SourceAppSelectionSet.Entries exceeds %d entries", MaxSourceAppSelectionEntries)
	}
	return nil
}

func validateSourceAppSelectionCanonicalSize(size uint64) error {
	if size > MaxCanonicalSourceAppSelectionSetBytes {
		return fmt.Errorf(
			"SourceAppSelectionSet canonical bytes exceed %d",
			MaxCanonicalSourceAppSelectionSetBytes,
		)
	}
	return nil
}

func canonicalSourceAppSelectionSetSize(set SourceAppSelectionSet) uint64 {
	size := sourceAppSelectionStringSize(set.SourceAppSelectionSetVersion) +
		sourceAppSelectionUintSize(uint64(len(set.Entries)))
	for _, entry := range set.Entries {
		size += sourceAppSelectionStringSize(entry.AppID)
		size += sourceAppSelectionStringSize(entry.AppInstallationID)
		size += sourceAppSelectionStringSize(entry.AppReleaseDigest)
		size++ // SelectionKind.
		size++ // Optional presence tag.
		if entry.AppLifecycleCandidateCommandID != "" {
			size += sourceAppSelectionStringSize(entry.AppLifecycleCandidateCommandID)
		}
	}
	return size
}

func sourceAppSelectionStringSize(value string) uint64 {
	return sourceAppSelectionUintSize(uint64(len(value))) + uint64(len(value))
}

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

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

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