package permissions

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"
	"sync"
	"unicode/utf8"

	"github.com/nexuscrm/shared/pkg/models"
	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

// PermissionSetAssignment.AssigneeId is utf8mb4 VARCHAR(255), so its width is
// measured in Unicode code points rather than UTF-8 bytes. The valid-UTF-8
// check below also means this bound permits at most 1020 bytes, which remains
// inside the wider managed physical-key envelope used for lock identities.
const assignmentPolicyMaximumAssigneeIDCharacters = 255

// AssignmentPolicyEligibilityResolution is the adapter-visible outcome of
// interpreting opaque selector keys. Definition drift remains owned by the L0
// policy engine and is therefore not an adapter result.
type AssignmentPolicyEligibilityResolution string

const (
	// AssignmentPolicyEligibilityResolved means every selector key was valid and
	// SubjectIDs is the complete current projection.
	AssignmentPolicyEligibilityResolved AssignmentPolicyEligibilityResolution = "resolved"
	// AssignmentPolicyEligibilityInvalidSelector means at least one selector key
	// does not currently resolve. SubjectIDs must be empty.
	AssignmentPolicyEligibilityInvalidSelector AssignmentPolicyEligibilityResolution = "invalid_selector"
)

// AssignmentPolicyEligibilityProjection is a product-neutral, current
// projection from opaque selector keys to opaque eligibility subject IDs.
// The L0 engine validates and canonicalizes SubjectIDs before using them.
type AssignmentPolicyEligibilityProjection struct {
	Resolution AssignmentPolicyEligibilityResolution
	SubjectIDs []string
}

// AssignmentPolicyEligibilityProjectionProvider is the only product seam used
// to interpret assignment-policy selector keys. The engine supplies a
// read-only facade: the provider cannot lock source rows or mutate the caller's
// transaction through this interface or through the facade's dynamic type.
// Providers may issue concurrent reads through the facade; the facade
// serializes them onto the transaction-bound store and enforces one aggregate
// projection budget.
type AssignmentPolicyEligibilityProjectionProvider interface {
	// AssignmentPolicyEligibilityReadSurfaces declares the product tables the
	// provider may read so the managed lifecycle inventory remains exact.
	AssignmentPolicyEligibilityReadSurfaces() []cp.ComponentReadSurface
	ProjectAssignmentPolicyEligibility(
		context.Context,
		cp.ComponentManagedReadStore,
		[]string,
	) (AssignmentPolicyEligibilityProjection, error)
}

type catalogAssignmentPolicyEligibilitySource struct {
	role                 models.AssignmentPolicyEligibilitySourceRole
	table                string
	idColumn             string
	subjectIDColumn      string
	selectorColumn       string
	referenceColumn      string
	targetTable          string
	targetIDColumn       string
	targetSelectorColumn string
	enabledEqual         cp.ComponentRow
}

type catalogAssignmentPolicyEligibilityProjectionProvider struct {
	authority   catalogAssignmentPolicyEligibilitySource
	populations []catalogAssignmentPolicyEligibilitySource
}

// NewCatalogAssignmentPolicyEligibilityProjectionProvider constructs the one
// app-blind eligibility read path from Catalog-compiled physical metadata. A
// nil provider means this runtime view declares no assignment-policy
// eligibility lifecycle at all. Partial or ambiguous declarations fail closed.
func NewCatalogAssignmentPolicyEligibilityProjectionProvider(
	objects []*models.ObjectMetadata,
) (AssignmentPolicyEligibilityProjectionProvider, error) {
	var authority *catalogAssignmentPolicyEligibilitySource
	populations := make([]catalogAssignmentPolicyEligibilitySource, 0)
	for _, object := range objects {
		if object == nil || object.AccessPolicy == nil || object.AccessPolicy.Lifecycle == nil ||
			object.AccessPolicy.Lifecycle.AssignmentPolicyEligibility == nil {
			continue
		}
		config := object.AccessPolicy.Lifecycle.AssignmentPolicyEligibility
		source, err := catalogAssignmentPolicyEligibilitySourceFromConfig(config)
		if err != nil {
			return nil, fmt.Errorf("assignment-policy eligibility Catalog source %s: %w", object.APIName, err)
		}
		switch source.role {
		case models.AssignmentPolicyEligibilitySelectorAuthority:
			if authority != nil {
				return nil, fmt.Errorf("assignment-policy eligibility Catalog declares multiple selector authorities")
			}
			frozen := source
			authority = &frozen
		case models.AssignmentPolicyEligibilitySubjectPopulation:
			populations = append(populations, source)
		default:
			return nil, fmt.Errorf("assignment-policy eligibility Catalog source has unsupported role %q", source.role)
		}
	}
	if authority == nil && len(populations) == 0 {
		return nil, nil
	}
	if authority == nil || len(populations) == 0 {
		return nil, fmt.Errorf("assignment-policy eligibility Catalog requires one selector authority and at least one subject population")
	}
	for _, population := range populations {
		if population.targetTable != authority.table ||
			population.targetIDColumn != authority.idColumn ||
			population.targetSelectorColumn != authority.selectorColumn {
			return nil, fmt.Errorf(
				"assignment-policy subject population target does not match the selector authority physical read surface",
			)
		}
	}
	sort.Slice(populations, func(i, j int) bool {
		if populations[i].table != populations[j].table {
			return populations[i].table < populations[j].table
		}
		return populations[i].subjectIDColumn < populations[j].subjectIDColumn
	})
	return &catalogAssignmentPolicyEligibilityProjectionProvider{
		authority: *authority, populations: populations,
	}, nil
}

func catalogAssignmentPolicyEligibilitySourceFromConfig(
	config *models.AssignmentPolicyEligibilityLifecyclePolicy,
) (catalogAssignmentPolicyEligibilitySource, error) {
	if config == nil {
		return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("configuration is absent")
	}
	source := catalogAssignmentPolicyEligibilitySource{
		role:            config.Role,
		table:           strings.TrimSpace(config.ResolvedSourceTable),
		idColumn:        strings.TrimSpace(config.ResolvedSourceIDColumn),
		subjectIDColumn: strings.TrimSpace(config.ResolvedSubjectIDColumn),
		selectorColumn:  strings.TrimSpace(config.Selector.ResolvedFieldColumn),
		enabledEqual:    make(cp.ComponentRow, len(config.EnabledWhen)),
	}
	if source.table == "" || source.idColumn == "" {
		return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("compiled source table and ID column are required")
	}
	for _, condition := range config.EnabledWhen {
		column := strings.TrimSpace(condition.ResolvedColumn)
		if column == "" {
			return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("compiled enabled-condition column is required")
		}
		if _, duplicate := source.enabledEqual[column]; duplicate {
			return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("compiled enabled-condition column %q is duplicated", column)
		}
		source.enabledEqual[column] = condition.Equals
	}
	if len(source.enabledEqual) == 0 {
		return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("at least one enabled condition is required")
	}
	switch source.role {
	case models.AssignmentPolicyEligibilitySelectorAuthority:
		if source.selectorColumn == "" || config.Selector.Reference != nil || source.subjectIDColumn != "" {
			return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("selector authority has an incomplete compiled direct selector")
		}
	case models.AssignmentPolicyEligibilitySubjectPopulation:
		if config.Selector.Reference == nil || source.subjectIDColumn == "" || source.selectorColumn != "" {
			return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("subject population has an incomplete compiled reference selector")
		}
		source.referenceColumn = strings.TrimSpace(config.Selector.Reference.ResolvedSourceFieldColumn)
		source.targetTable = strings.TrimSpace(config.Selector.Reference.ResolvedTargetTable)
		source.targetIDColumn = strings.TrimSpace(config.Selector.Reference.ResolvedTargetIDColumn)
		source.targetSelectorColumn = strings.TrimSpace(config.Selector.Reference.ResolvedTargetFieldColumn)
		if source.referenceColumn == "" || source.targetTable == "" || source.targetIDColumn == "" || source.targetSelectorColumn == "" {
			return catalogAssignmentPolicyEligibilitySource{}, fmt.Errorf("subject population compiled reference columns are required")
		}
	}
	return source, nil
}

func (p *catalogAssignmentPolicyEligibilityProjectionProvider) AssignmentPolicyEligibilityReadSurfaces() []cp.ComponentReadSurface {
	byTable := map[string]string{
		p.authority.table: "assignment-policy selector authority",
	}
	for _, population := range p.populations {
		byTable[population.table] = "assignment-policy subject population"
	}
	tables := make([]string, 0, len(byTable))
	for table := range byTable {
		tables = append(tables, table)
	}
	sort.Strings(tables)
	surfaces := make([]cp.ComponentReadSurface, 0, len(tables))
	for _, table := range tables {
		surfaces = append(surfaces, cp.ComponentReadSurface{Table: table, Fact: byTable[table]})
	}
	return surfaces
}

func (p *catalogAssignmentPolicyEligibilityProjectionProvider) ProjectAssignmentPolicyEligibility(
	ctx context.Context,
	store cp.ComponentManagedReadStore,
	selectorKeys []string,
) (AssignmentPolicyEligibilityProjection, error) {
	paged, ok := store.(cp.ComponentManagedKeysetTransactionReadStore)
	if !ok {
		return AssignmentPolicyEligibilityProjection{}, fmt.Errorf("Catalog assignment-policy eligibility requires exact-exhaustion reads")
	}
	orderedSelectors := append([]string(nil), selectorKeys...)
	sort.Slice(orderedSelectors, func(i, j int) bool {
		left := assignmentPolicyDatabaseIdentity(orderedSelectors[i])
		right := assignmentPolicyDatabaseIdentity(orderedSelectors[j])
		if left != right {
			return left < right
		}
		return orderedSelectors[i] < orderedSelectors[j]
	})
	subjectSet := make(map[string]struct{})
	for _, selector := range orderedSelectors {
		authorityID, valid, err := p.resolveAuthorityID(ctx, paged, selector)
		if err != nil {
			return AssignmentPolicyEligibilityProjection{}, err
		}
		if !valid {
			return AssignmentPolicyEligibilityProjection{Resolution: AssignmentPolicyEligibilityInvalidSelector}, nil
		}
		for _, population := range p.populations {
			if err := readCatalogAssignmentPolicyPopulation(ctx, paged, population, authorityID, subjectSet); err != nil {
				return AssignmentPolicyEligibilityProjection{}, err
			}
		}
	}
	subjectIDs := make([]string, 0, len(subjectSet))
	for subjectID := range subjectSet {
		subjectIDs = append(subjectIDs, subjectID)
	}
	sortAssignmentPolicySubjectIDs(subjectIDs)
	return AssignmentPolicyEligibilityProjection{
		Resolution: AssignmentPolicyEligibilityResolved,
		SubjectIDs: subjectIDs,
	}, nil
}

func (p *catalogAssignmentPolicyEligibilityProjectionProvider) resolveAuthorityID(
	ctx context.Context,
	store cp.ComponentManagedKeysetTransactionReadStore,
	selector string,
) (string, bool, error) {
	equal := cloneCatalogAssignmentPolicyEqual(p.authority.enabledEqual)
	equal[p.authority.selectorColumn] = selector
	page, err := store.ReadRowsKeysetPage(ctx, cp.ComponentManagedKeysetRowQuery{Query: cp.ComponentManagedRowQuery{
		Table:             p.authority.table,
		Columns:           []string{p.authority.idColumn},
		Equal:             equal,
		OrderBy:           []cp.ComponentManagedOrder{{Column: p.authority.idColumn, Direction: cp.ComponentManagedOrderAscending}},
		LogicalKeyColumns: []string{p.authority.idColumn},
		MaximumRows:       2,
	}})
	if err != nil {
		return "", false, err
	}
	if !page.Exhausted {
		return "", false, fmt.Errorf("assignment-policy selector authority read did not prove exhaustion")
	}
	if len(page.Rows) == 0 {
		return "", false, nil
	}
	if len(page.Rows) != 1 {
		return "", false, nil
	}
	authorityID, err := assignmentPolicyRequiredString(page.Rows[0], p.authority.idColumn)
	return authorityID, err == nil, err
}

func readCatalogAssignmentPolicyPopulation(
	ctx context.Context,
	store cp.ComponentManagedKeysetTransactionReadStore,
	population catalogAssignmentPolicyEligibilitySource,
	authorityID string,
	subjectSet map[string]struct{},
) error {
	equal := cloneCatalogAssignmentPolicyEqual(population.enabledEqual)
	equal[population.referenceColumn] = authorityID
	var after cp.ComponentRow
	for {
		page, err := store.ReadRowsKeysetPage(ctx, cp.ComponentManagedKeysetRowQuery{
			Query: cp.ComponentManagedRowQuery{
				Table:             population.table,
				Columns:           []string{population.subjectIDColumn},
				Equal:             equal,
				OrderBy:           []cp.ComponentManagedOrder{{Column: population.subjectIDColumn, Direction: cp.ComponentManagedOrderAscending}},
				LogicalKeyColumns: []string{population.subjectIDColumn},
				MaximumRows:       min(500, assignmentPolicyMaximumRows-len(subjectSet)+1),
			},
			ContinueAfter: after,
		})
		if err != nil {
			return err
		}
		if len(page.Rows) == 0 && !page.Exhausted {
			return fmt.Errorf("assignment-policy subject population read did not advance")
		}
		for _, row := range page.Rows {
			subjectID, valueErr := assignmentPolicyRequiredString(row, population.subjectIDColumn)
			if valueErr != nil {
				return valueErr
			}
			subjectSet[subjectID] = struct{}{}
			if len(subjectSet) > assignmentPolicyMaximumRows {
				return fmt.Errorf("assignment-policy eligibility population exceeds maximum %d", assignmentPolicyMaximumRows)
			}
		}
		if page.Exhausted {
			return nil
		}
		last := page.Rows[len(page.Rows)-1]
		after = cp.ComponentRow{population.subjectIDColumn: last[population.subjectIDColumn]}
	}
}

func cloneCatalogAssignmentPolicyEqual(source cp.ComponentRow) cp.ComponentRow {
	clone := make(cp.ComponentRow, len(source)+1)
	for column, value := range source {
		clone[column] = value
	}
	return clone
}

type assignmentPolicyEligibilityReadOnlyStore struct {
	store  cp.ComponentManagedReadStore
	budget *assignmentPolicyEligibilityReadBudget
}

type assignmentPolicyEligibilityReadBudget struct {
	// The host-owned store meter remains authoritative across locked replan and
	// mutation. This ledger closes the narrower seam invariant: multiple
	// eligibility passes inside one permissions callback cannot each reset to the
	// registration maximum, even for a test or adapter store without host
	// metering.
	readMu sync.Mutex
	mu     sync.Mutex

	maximumItems  int
	observedItems int
	budgetErr     error
}

func newAssignmentPolicyEligibilityReadBudget(maximumItems int) *assignmentPolicyEligibilityReadBudget {
	return &assignmentPolicyEligibilityReadBudget{maximumItems: maximumItems}
}

func (s *assignmentPolicyEligibilityReadOnlyStore) ReadRows(
	ctx context.Context,
	query cp.ComponentManagedRowQuery,
) ([]cp.ComponentRow, error) {
	s.budget.readMu.Lock()
	defer s.budget.readMu.Unlock()
	maximumRows, err := s.budget.remainingMaximum(query.MaximumRows)
	if err != nil {
		return nil, err
	}
	query.MaximumRows = maximumRows
	rows, err := s.store.ReadRows(ctx, query)
	if err != nil {
		return nil, err
	}
	if err := s.budget.observe(len(rows)); err != nil {
		return nil, err
	}
	return rows, nil
}

func (s *assignmentPolicyEligibilityReadOnlyStore) ReadRowsPage(
	ctx context.Context,
	query cp.ComponentManagedKeysetRowQuery,
) ([]cp.ComponentRow, error) {
	s.budget.readMu.Lock()
	defer s.budget.readMu.Unlock()
	paged, ok := s.store.(cp.ComponentManagedTransactionReadStore)
	if !ok {
		return nil, fmt.Errorf("assignment policy eligibility store does not support paged reads")
	}
	maximumRows, err := s.budget.remainingMaximum(query.Query.MaximumRows)
	if err != nil {
		return nil, err
	}
	query.Query.MaximumRows = maximumRows
	rows, err := paged.ReadRowsPage(ctx, query)
	if err != nil {
		return nil, err
	}
	if err := s.budget.observe(len(rows)); err != nil {
		return nil, err
	}
	return rows, nil
}

func (s *assignmentPolicyEligibilityReadOnlyStore) ReadRowsKeysetPage(
	ctx context.Context,
	query cp.ComponentManagedKeysetRowQuery,
) (cp.ComponentManagedKeysetRowPage, error) {
	s.budget.readMu.Lock()
	defer s.budget.readMu.Unlock()
	paged, ok := s.store.(cp.ComponentManagedKeysetTransactionReadStore)
	if !ok {
		return cp.ComponentManagedKeysetRowPage{}, fmt.Errorf(
			"assignment policy eligibility store does not support exact-exhaustion reads",
		)
	}
	maximumRows, err := s.budget.remainingMaximum(query.Query.MaximumRows)
	if err != nil {
		return cp.ComponentManagedKeysetRowPage{}, err
	}
	query.Query.MaximumRows = maximumRows
	page, err := paged.ReadRowsKeysetPage(ctx, query)
	if err != nil {
		return cp.ComponentManagedKeysetRowPage{}, err
	}
	if err := s.budget.observe(len(page.Rows)); err != nil {
		return cp.ComponentManagedKeysetRowPage{}, err
	}
	return page, nil
}

func (b *assignmentPolicyEligibilityReadBudget) remainingMaximum(requested int) (int, error) {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.budgetErr != nil {
		return 0, b.budgetErr
	}
	remaining := b.maximumItems - b.observedItems
	if remaining <= 0 {
		// Managed row queries cannot declare a zero-row bound. Permit one
		// sentinel row so an empty predicate can still be proved; observe rejects
		// the sentinel before it becomes provider-visible when the budget is full.
		return min(requested, 1), nil
	}
	if requested > remaining {
		return remaining, nil
	}
	return requested, nil
}

func (b *assignmentPolicyEligibilityReadBudget) observe(items int) error {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.budgetErr != nil {
		return b.budgetErr
	}
	b.observedItems += items
	if b.observedItems > b.maximumItems {
		b.budgetErr = fmt.Errorf(
			"assignment policy eligibility projection items %d exceed maximum %d",
			b.observedItems,
			b.maximumItems,
		)
		return b.budgetErr
	}
	return nil
}

func (b *assignmentPolicyEligibilityReadBudget) observation() (int, error) {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.observedItems, b.budgetErr
}

func (b *assignmentPolicyEligibilityReadBudget) observationSince(start int) (int, error) {
	observedItems, err := b.observation()
	if observedItems < start {
		return 0, fmt.Errorf("assignment policy eligibility projection budget regressed")
	}
	return observedItems - start, err
}

func projectAssignmentPolicyEligibility(
	ctx context.Context,
	store cp.ComponentManagedReadStore,
	targetID string,
	selectorKeys []string,
	provider AssignmentPolicyEligibilityProjectionProvider,
	budget *assignmentPolicyEligibilityReadBudget,
) (string, []string, []string, int, error) {
	if assignmentPolicyEligibilityProjectionProviderIsNil(provider) {
		return "", nil, nil, 0, fmt.Errorf(
			"assignment policy eligibility projection provider is required",
		)
	}
	if budget == nil {
		return "", nil, nil, 0, fmt.Errorf(
			"assignment policy eligibility projection budget is required",
		)
	}
	if budget.maximumItems < 0 {
		return "", nil, nil, 0, fmt.Errorf(
			"assignment policy eligibility projection maximum is negative",
		)
	}
	startingItems, budgetErr := budget.observation()
	if budgetErr != nil {
		return "", nil, nil, 0, budgetErr
	}
	readOnly := &assignmentPolicyEligibilityReadOnlyStore{
		store: store, budget: budget,
	}
	negativeEdges, err := assignmentPolicyNegativeEdges()
	if err != nil {
		return "", nil, nil, 0, err
	}
	for _, relation := range negativeEdges {
		exists, readErr := assignmentPolicyNegativeEdgeExists(ctx, readOnly, relation, targetID)
		if readErr != nil {
			observedItems, _ := budget.observationSince(startingItems)
			return "", nil, nil, observedItems, readErr
		}
		if exists {
			observedItems, budgetErr := budget.observationSince(startingItems)
			if budgetErr != nil {
				return "", nil, nil, observedItems, budgetErr
			}
			return assignmentPolicyResolutionUnsupportedDrift,
				append([]string(nil), selectorKeys...), nil, observedItems, nil
		}
	}
	if len(selectorKeys) == 0 {
		observedItems, budgetErr := budget.observationSince(startingItems)
		if budgetErr != nil {
			return "", nil, nil, observedItems, budgetErr
		}
		return assignmentPolicyResolutionInvalidSelector,
			[]string{}, nil, observedItems, nil
	}
	projection, err := provider.ProjectAssignmentPolicyEligibility(
		ctx, readOnly, append([]string(nil), selectorKeys...),
	)
	observedItems, budgetErr := budget.observationSince(startingItems)
	if err != nil {
		return "", nil, nil, observedItems,
			fmt.Errorf("assignment policy eligibility projection: %w", err)
	}
	if budgetErr != nil {
		return "", nil, nil, observedItems, budgetErr
	}
	subjectIDs, err := canonicalAssignmentPolicyEligibilitySubjectIDs(projection.SubjectIDs)
	if err != nil {
		return "", nil, nil, observedItems, err
	}
	switch projection.Resolution {
	case AssignmentPolicyEligibilityResolved:
		return assignmentPolicyResolutionResolved,
			append([]string(nil), selectorKeys...), subjectIDs, observedItems, nil
	case AssignmentPolicyEligibilityInvalidSelector:
		if len(subjectIDs) != 0 {
			return "", nil, nil, observedItems, fmt.Errorf(
				"assignment policy invalid-selector projection returned subject IDs",
			)
		}
		return assignmentPolicyResolutionInvalidSelector,
			append([]string(nil), selectorKeys...), nil, observedItems, nil
	default:
		return "", nil, nil, observedItems, fmt.Errorf(
			"assignment policy eligibility projection returned unsupported resolution %q",
			projection.Resolution,
		)
	}
}

func assignmentPolicyEligibilityProjectionProviderIsNil(
	provider AssignmentPolicyEligibilityProjectionProvider,
) bool {
	if provider == nil {
		return true
	}
	value := reflect.ValueOf(provider)
	switch value.Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
		return value.IsNil()
	default:
		return false
	}
}

func canonicalAssignmentPolicyEligibilitySubjectIDs(subjectIDs []string) ([]string, error) {
	if len(subjectIDs) > assignmentPolicyMaximumRows {
		return nil, fmt.Errorf(
			"assignment policy eligibility projection contains %d subjects; maximum is %d",
			len(subjectIDs), assignmentPolicyMaximumRows,
		)
	}
	byIdentity := make(map[string]string, len(subjectIDs))
	for _, subjectID := range subjectIDs {
		if !utf8.ValidString(subjectID) {
			return nil, fmt.Errorf("assignment policy eligibility subject ID must be valid UTF-8")
		}
		if strings.TrimSpace(subjectID) == "" {
			return nil, fmt.Errorf("assignment policy eligibility subject ID is empty")
		}
		if utf8.RuneCountInString(subjectID) > assignmentPolicyMaximumAssigneeIDCharacters {
			return nil, fmt.Errorf(
				"assignment policy eligibility subject ID exceeds %d characters for PermissionSetAssignment.AssigneeId",
				assignmentPolicyMaximumAssigneeIDCharacters,
			)
		}
		identity := assignmentPolicyDatabaseIdentity(subjectID)
		if previous, duplicate := byIdentity[identity]; duplicate && previous != subjectID {
			return nil, fmt.Errorf(
				"assignment policy eligibility subject IDs %q and %q collide under database collation",
				previous, subjectID,
			)
		}
		byIdentity[identity] = subjectID
	}
	canonical := make([]string, 0, len(byIdentity))
	for _, subjectID := range byIdentity {
		canonical = append(canonical, subjectID)
	}
	sortAssignmentPolicySubjectIDs(canonical)
	return canonical, nil
}

func sortAssignmentPolicySubjectIDs(subjectIDs []string) {
	sort.Slice(subjectIDs, func(i, j int) bool {
		left := assignmentPolicyDatabaseIdentity(subjectIDs[i])
		right := assignmentPolicyDatabaseIdentity(subjectIDs[j])
		if left != right {
			return left < right
		}
		return subjectIDs[i] < subjectIDs[j]
	})
}
