package catalogsnapshot

import (
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/nexuscrm/shared/pkg/models"
)

var (
	ErrPageLayoutNotFound          = errors.New("caller-effective page layout not found")
	ErrPageLayoutRecordTypeMissing = errors.New("page layout record type not found")
)

type PageLayoutInput struct {
	CatalogRevision CatalogRevision
	Layout          *models.PageLayout
}

type LayoutAssignmentInput struct {
	CatalogRevision CatalogRevision
	Assignment      *models.LayoutAssignment
}

// PageLayoutRecordTypeInput is the minimal active identity needed to reject a
// caller-supplied stale or foreign RecordType before applying wildcard routing.
type PageLayoutRecordTypeInput struct {
	CatalogRevision CatalogRevision
	ObjectAPIName   string
	RecordTypeID    string
}

// RelatedListResolutionInput carries only the neutral runtime target for one
// display identity. Recommendation, ordering, labels, and other L1 authoring
// facts stay in the PageLayout document.
type RelatedListResolutionInput struct {
	CatalogRevision    CatalogRevision
	ParentObjectID     string
	DisplayObjectID    string
	DisplayLookupField string
	Query              PageLayoutResolvedQueryTarget
	Detail             PageLayoutResolvedDetailTarget
	ListTarget         PageLayoutResolvedListTarget
	Create             PageLayoutResolvedCreateTarget
}

type PageLayoutResolvedQueryTarget struct {
	ObjectID      string `json:"objectId"`
	LookupField   string `json:"lookupField"`
	ParentFieldID string `json:"parentFieldId,omitempty"`
}

type PageLayoutResolvedDetailTarget struct {
	ObjectID string `json:"objectId"`
}

type PageLayoutResolvedListTarget struct {
	ObjectID      string `json:"objectId"`
	FilterField   string `json:"filterField"`
	ParentFieldID string `json:"parentFieldId,omitempty"`
}

type PageLayoutResolvedCreateTarget struct {
	Kind            string            `json:"kind"`
	ObjectID        string            `json:"objectId,omitempty"`
	PrefillMappings map[string]string `json:"prefillMappings,omitempty"`
}

type pageLayoutResolutionDocument struct {
	ObjectIDs    []string                      `json:"objectIds"`
	Layouts      []models.PageLayout           `json:"layouts"`
	Assignments  []models.LayoutAssignment     `json:"assignments"`
	RecordTypes  []pageLayoutRecordTypeRecord  `json:"recordTypes"`
	RelatedLists []pageLayoutRelatedListRecord `json:"relatedLists"`
}

type pageLayoutRecordTypeRecord struct {
	ObjectAPIName string `json:"objectApiName"`
	RecordTypeID  string `json:"recordTypeId"`
}

type pageLayoutRelatedListRecord struct {
	ParentObjectID     string                         `json:"parentObjectId"`
	DisplayObjectID    string                         `json:"displayObjectId"`
	DisplayLookupField string                         `json:"displayLookupField"`
	Query              PageLayoutResolvedQueryTarget  `json:"query"`
	Detail             PageLayoutResolvedDetailTarget `json:"detail"`
	ListTarget         PageLayoutResolvedListTarget   `json:"list"`
	Create             PageLayoutResolvedCreateTarget `json:"create"`
}

func inputHasPageLayoutResolutionProjection(values []RuntimeProjectionInput) bool {
	for _, value := range values {
		if value.Profile == ProjectionPageLayoutResolutionV1 {
			return true
		}
	}
	return false
}

func buildPageLayoutResolutionDocument(
	revision CatalogRevision,
	layoutInputs []PageLayoutInput,
	assignmentInputs []LayoutAssignmentInput,
	recordTypeInputs []PageLayoutRecordTypeInput,
	resolutionInputs []RelatedListResolutionInput,
	objects map[string]*models.ObjectMetadata,
) (*pageLayoutResolutionDocument, error) {
	if len(layoutInputs) == 0 && len(assignmentInputs) == 0 && len(recordTypeInputs) == 0 && len(resolutionInputs) == 0 {
		return nil, nil
	}
	doc := &pageLayoutResolutionDocument{
		ObjectIDs: []string{},
		Layouts:   []models.PageLayout{}, Assignments: []models.LayoutAssignment{},
		RecordTypes: []pageLayoutRecordTypeRecord{}, RelatedLists: []pageLayoutRelatedListRecord{},
	}
	for _, object := range objects {
		doc.ObjectIDs = append(doc.ObjectIDs, object.APIName)
	}
	sort.Slice(doc.ObjectIDs, func(i, j int) bool { return identityLess(doc.ObjectIDs[i], doc.ObjectIDs[j]) })
	layoutByID := make(map[string]models.PageLayout, len(layoutInputs))
	for index, input := range layoutInputs {
		path := fmt.Sprintf("pageLayoutResolution.layouts[%d]", index)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		if input.Layout == nil || input.Layout.IsDeleted {
			return nil, conflict(ConflictInvalidDefinition, path, "layout must be live")
		}
		layout, err := cloneJSON(*input.Layout)
		if err != nil {
			return nil, conflict(ConflictEncoding, path, "clone layout: %v", err)
		}
		if err := requireCanonicalIdentity(path+".id", layout.ID); err != nil {
			return nil, err
		}
		if err := requireCatalogObjectAPIName(path+".objectApiName", layout.ObjectAPIName); err != nil {
			return nil, err
		}
		if objects[foldIdentity(layout.ObjectAPIName)] == nil {
			return nil, conflict(ConflictInvalidDefinition, path+".objectApiName", "object %q is absent", layout.ObjectAPIName)
		}
		if err := requireCanonicalIdentity(path+".layoutName", layout.LayoutName); err != nil {
			return nil, err
		}
		key := foldIdentity(layout.ID)
		if _, duplicate := layoutByID[key]; duplicate {
			return nil, conflict(ConflictInvalidDefinition, path+".id", "layout %q is duplicated", layout.ID)
		}
		// Database lifecycle timestamps are not layout behavior and must not make
		// an otherwise identical immutable Catalog artifact change.
		layout.CreatedDate = time.Time{}
		layout.LastModifiedDate = time.Time{}
		layoutByID[key] = layout
		doc.Layouts = append(doc.Layouts, layout)
	}
	sort.Slice(doc.Layouts, func(i, j int) bool { return identityLess(doc.Layouts[i].ID, doc.Layouts[j].ID) })

	assignmentKeys := make(map[string]struct{}, len(assignmentInputs))
	for index, input := range assignmentInputs {
		path := fmt.Sprintf("pageLayoutResolution.assignments[%d]", index)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		if input.Assignment == nil {
			return nil, conflict(ConflictInvalidDefinition, path, "assignment is nil")
		}
		assignment, err := cloneJSON(*input.Assignment)
		if err != nil {
			return nil, conflict(ConflictEncoding, path, "clone assignment: %v", err)
		}
		assignment.Normalize()
		if err := validatePageLayoutAssignment(path, &assignment, layoutByID, objects); err != nil {
			return nil, err
		}
		key := pageLayoutAssignmentTupleKey(assignment)
		if _, duplicate := assignmentKeys[key]; duplicate {
			return nil, conflict(ConflictInvalidDefinition, path, "assignment dimensions are duplicated")
		}
		assignmentKeys[key] = struct{}{}
		assignment.CreatedDate = time.Time{}
		assignment.LastModifiedDate = time.Time{}
		doc.Assignments = append(doc.Assignments, assignment)
	}
	sort.Slice(doc.Assignments, func(i, j int) bool {
		return pageLayoutAssignmentTupleKey(doc.Assignments[i]) < pageLayoutAssignmentTupleKey(doc.Assignments[j])
	})

	recordTypeKeys := make(map[string]struct{}, len(recordTypeInputs))
	for index, input := range recordTypeInputs {
		path := fmt.Sprintf("pageLayoutResolution.recordTypes[%d]", index)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		if err := requireCatalogObjectAPIName(path+".objectApiName", input.ObjectAPIName); err != nil {
			return nil, err
		}
		if objects[foldIdentity(input.ObjectAPIName)] == nil {
			return nil, conflict(ConflictInvalidDefinition, path+".objectApiName", "object %q is absent", input.ObjectAPIName)
		}
		if err := requireCanonicalIdentity(path+".recordTypeId", input.RecordTypeID); err != nil {
			return nil, err
		}
		key := foldIdentity(input.ObjectAPIName) + "\x00" + foldIdentity(input.RecordTypeID)
		if _, duplicate := recordTypeKeys[key]; duplicate {
			return nil, conflict(ConflictInvalidDefinition, path, "record type identity is duplicated")
		}
		recordTypeKeys[key] = struct{}{}
		doc.RecordTypes = append(doc.RecordTypes, pageLayoutRecordTypeRecord{ObjectAPIName: input.ObjectAPIName, RecordTypeID: input.RecordTypeID})
	}
	sort.Slice(doc.RecordTypes, func(i, j int) bool {
		left := foldIdentity(doc.RecordTypes[i].ObjectAPIName) + "\x00" + foldIdentity(doc.RecordTypes[i].RecordTypeID)
		right := foldIdentity(doc.RecordTypes[j].ObjectAPIName) + "\x00" + foldIdentity(doc.RecordTypes[j].RecordTypeID)
		return left < right
	})

	resolutionKeys := make(map[string]struct{}, len(resolutionInputs))
	for index, input := range resolutionInputs {
		path := fmt.Sprintf("pageLayoutResolution.relatedLists[%d]", index)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		record := pageLayoutRelatedListRecord{
			ParentObjectID: input.ParentObjectID, DisplayObjectID: input.DisplayObjectID,
			DisplayLookupField: input.DisplayLookupField, Query: input.Query,
			Detail: input.Detail, ListTarget: input.ListTarget, Create: input.Create,
		}
		if err := validatePageLayoutRelatedList(path, record, objects); err != nil {
			return nil, err
		}
		key := pageLayoutRelatedListKey(record)
		if _, duplicate := resolutionKeys[key]; duplicate {
			return nil, conflict(ConflictInvalidDefinition, path, "related-list display identity is duplicated")
		}
		resolutionKeys[key] = struct{}{}
		doc.RelatedLists = append(doc.RelatedLists, record)
	}
	sort.Slice(doc.RelatedLists, func(i, j int) bool {
		return pageLayoutRelatedListKey(doc.RelatedLists[i]) < pageLayoutRelatedListKey(doc.RelatedLists[j])
	})
	return doc, nil
}

func validatePageLayoutAssignment(path string, assignment *models.LayoutAssignment, layouts map[string]models.PageLayout, objects map[string]*models.ObjectMetadata) error {
	if err := requireCanonicalIdentity(path+".id", assignment.ID); err != nil {
		return err
	}
	if err := requireCatalogObjectAPIName(path+".objectApiName", assignment.ObjectAPIName); err != nil {
		return err
	}
	if objects[foldIdentity(assignment.ObjectAPIName)] == nil {
		return conflict(ConflictInvalidDefinition, path+".objectApiName", "object %q is absent", assignment.ObjectAPIName)
	}
	if err := requireCanonicalIdentity(path+".layoutId", assignment.LayoutID); err != nil {
		return err
	}
	layout, exists := layouts[foldIdentity(assignment.LayoutID)]
	if !exists || !strings.EqualFold(layout.ObjectAPIName, assignment.ObjectAPIName) {
		return conflict(ConflictInvalidDefinition, path+".layoutId", "assignment target is missing or belongs to another object")
	}
	for name, value := range map[string]*string{"roleId": assignment.RoleID, "profileId": assignment.ProfileID, "recordTypeId": assignment.RecordTypeID} {
		if value != nil {
			if err := requireCanonicalIdentity(path+"."+name, *value); err != nil {
				return err
			}
		}
	}
	expected := 0
	if assignment.RoleID != nil {
		expected++
	}
	if assignment.ProfileID != nil {
		expected++
	}
	if assignment.RecordTypeID != nil {
		expected++
	}
	if assignment.Specificity != expected {
		return conflict(ConflictInvalidDefinition, path+".specificity", "specificity %d does not match dimensions %d", assignment.Specificity, expected)
	}
	return nil
}

func validatePageLayoutRelatedList(path string, value pageLayoutRelatedListRecord, objects map[string]*models.ObjectMetadata) error {
	objectIDs := []string{value.ParentObjectID, value.DisplayObjectID, value.Query.ObjectID, value.Detail.ObjectID, value.ListTarget.ObjectID}
	if value.Create.Kind == "standard" {
		objectIDs = append(objectIDs, value.Create.ObjectID)
	}
	for _, objectID := range objectIDs {
		if err := requireCatalogObjectAPIName(path+".objectId", objectID); err != nil {
			return err
		}
		if objects[foldIdentity(objectID)] == nil {
			return conflict(ConflictInvalidDefinition, path+".objectId", "object %q is absent", objectID)
		}
	}
	for name, field := range map[string]string{
		"displayLookupField": value.DisplayLookupField, "query.lookupField": value.Query.LookupField,
		"list.filterField": value.ListTarget.FilterField,
	} {
		if err := requireCanonicalIdentity(path+"."+name, field); err != nil {
			return err
		}
	}
	for name, field := range map[string]string{"query.parentFieldId": value.Query.ParentFieldID, "list.parentFieldId": value.ListTarget.ParentFieldID} {
		if field != "" {
			if err := requireCanonicalIdentity(path+"."+name, field); err != nil {
				return err
			}
		}
	}
	switch value.Create.Kind {
	case "standard":
		if value.Create.ObjectID == "" {
			return conflict(ConflictInvalidDefinition, path+".create", "standard create requires objectId")
		}
	case "upload", "none":
		if value.Create.ObjectID != "" {
			return conflict(ConflictInvalidDefinition, path+".create", "%s create must not carry objectId", value.Create.Kind)
		}
	default:
		return conflict(ConflictInvalidDefinition, path+".create.kind", "unsupported create kind %q", value.Create.Kind)
	}
	for target, source := range value.Create.PrefillMappings {
		if err := requireCanonicalIdentity(path+".create.prefillMappings.target", target); err != nil {
			return err
		}
		if err := requireCanonicalIdentity(path+".create.prefillMappings.source", source); err != nil {
			return err
		}
	}
	return nil
}

func pageLayoutAssignmentTupleKey(value models.LayoutAssignment) string {
	return foldIdentity(value.ObjectAPIName) + "\x00" + optionalFold(value.RecordTypeID) + "\x00" + optionalFold(value.ProfileID) + "\x00" + optionalFold(value.RoleID)
}

func pageLayoutRelatedListKey(value pageLayoutRelatedListRecord) string {
	return foldIdentity(value.ParentObjectID) + "\x00" + foldIdentity(value.DisplayObjectID) + "\x00" + foldIdentity(value.DisplayLookupField)
}

func optionalFold(value *string) string {
	if value == nil {
		return ""
	}
	return foldIdentity(*value)
}

func projectPageLayoutResolution(source *pageLayoutResolutionDocument, included map[string]struct{}) *pageLayoutResolutionDocument {
	if source == nil {
		return nil
	}
	result := &pageLayoutResolutionDocument{
		ObjectIDs: []string{},
		Layouts:   []models.PageLayout{}, Assignments: []models.LayoutAssignment{},
		RecordTypes: []pageLayoutRecordTypeRecord{}, RelatedLists: []pageLayoutRelatedListRecord{},
	}
	for _, objectID := range source.ObjectIDs {
		if _, ok := included[foldIdentity(objectID)]; ok {
			result.ObjectIDs = append(result.ObjectIDs, objectID)
		}
	}
	for _, layout := range source.Layouts {
		if _, ok := included[foldIdentity(layout.ObjectAPIName)]; ok {
			result.Layouts = append(result.Layouts, layout)
		}
	}
	for _, assignment := range source.Assignments {
		if _, ok := included[foldIdentity(assignment.ObjectAPIName)]; ok {
			result.Assignments = append(result.Assignments, assignment)
		}
	}
	for _, recordType := range source.RecordTypes {
		if _, ok := included[foldIdentity(recordType.ObjectAPIName)]; ok {
			result.RecordTypes = append(result.RecordTypes, recordType)
		}
	}
	for _, related := range source.RelatedLists {
		if _, ok := included[foldIdentity(related.ParentObjectID)]; ok {
			result.RelatedLists = append(result.RelatedLists, related)
		}
	}
	return result
}

func validatePageLayoutResolutionDocument(doc *pageLayoutResolutionDocument, sourceObjects map[string]*models.ObjectMetadata, schema string) error {
	if doc == nil {
		return nil
	}
	if schema != CatalogSnapshotSchemaV17 && schema != CatalogSnapshotSchemaV18 && schema != CatalogSnapshotSchemaV19 && (schema != CatalogSnapshotSchemaV20 && schema != CatalogSnapshotSchemaV21) && schema != RuntimeMetadataViewSchemaV23 {
		return fmt.Errorf("pre-v17 metadata artifact contains page-layout resolution authority")
	}
	if len(doc.ObjectIDs) == 0 || len(doc.Layouts) == 0 || len(doc.Assignments) == 0 {
		return fmt.Errorf("page-layout resolution closure is incomplete")
	}
	objects := make(map[string]*models.ObjectMetadata, len(doc.ObjectIDs))
	previous := ""
	for index, objectID := range doc.ObjectIDs {
		if err := requireCatalogObjectAPIName(fmt.Sprintf("pageLayoutResolution.objectIds[%d]", index), objectID); err != nil {
			return err
		}
		key := foldIdentity(objectID)
		if key <= previous {
			return fmt.Errorf("pageLayoutResolution.objectIds is not canonical")
		}
		previous = key
		if sourceObjects != nil {
			if sourceObjects[key] == nil {
				return fmt.Errorf("pageLayoutResolution.objectIds[%d] references absent object %q", index, objectID)
			}
			objects[key] = sourceObjects[key]
		} else {
			objects[key] = &models.ObjectMetadata{APIName: objectID}
		}
	}
	layouts := make(map[string]models.PageLayout, len(doc.Layouts))
	previous = ""
	for index := range doc.Layouts {
		layout := doc.Layouts[index]
		path := fmt.Sprintf("pageLayoutResolution.layouts[%d]", index)
		if layout.IsDeleted || !layout.CreatedDate.IsZero() || !layout.LastModifiedDate.IsZero() {
			return fmt.Errorf("%s contains lifecycle state", path)
		}
		if err := requireCanonicalIdentity(path+".id", layout.ID); err != nil {
			return err
		}
		if err := requireCatalogObjectAPIName(path+".objectApiName", layout.ObjectAPIName); err != nil {
			return err
		}
		if objects[foldIdentity(layout.ObjectAPIName)] == nil {
			return fmt.Errorf("%s references absent object", path)
		}
		if err := requireCanonicalIdentity(path+".layoutName", layout.LayoutName); err != nil {
			return err
		}
		key := foldIdentity(layout.ID)
		if key <= previous {
			return fmt.Errorf("pageLayoutResolution.layouts is not canonical")
		}
		previous = key
		layouts[key] = layout
	}
	recordTypes := make(map[string]struct{}, len(doc.RecordTypes))
	previous = ""
	for index, recordType := range doc.RecordTypes {
		path := fmt.Sprintf("pageLayoutResolution.recordTypes[%d]", index)
		if err := requireCatalogObjectAPIName(path+".objectApiName", recordType.ObjectAPIName); err != nil {
			return err
		}
		if objects[foldIdentity(recordType.ObjectAPIName)] == nil {
			return fmt.Errorf("%s references absent object", path)
		}
		if err := requireCanonicalIdentity(path+".recordTypeId", recordType.RecordTypeID); err != nil {
			return err
		}
		key := foldIdentity(recordType.ObjectAPIName) + "\x00" + foldIdentity(recordType.RecordTypeID)
		if key <= previous {
			return fmt.Errorf("pageLayoutResolution.recordTypes is not canonical")
		}
		previous = key
		recordTypes[key] = struct{}{}
	}
	previous = ""
	for index := range doc.Assignments {
		assignment := &doc.Assignments[index]
		path := fmt.Sprintf("pageLayoutResolution.assignments[%d]", index)
		if !assignment.CreatedDate.IsZero() || !assignment.LastModifiedDate.IsZero() {
			return fmt.Errorf("%s contains lifecycle timestamps", path)
		}
		if err := validatePageLayoutAssignment(path, assignment, layouts, objects); err != nil {
			return err
		}
		key := pageLayoutAssignmentTupleKey(*assignment)
		if key <= previous {
			return fmt.Errorf("pageLayoutResolution.assignments is not canonical")
		}
		previous = key
		if assignment.RecordTypeID != nil {
			recordTypeKey := foldIdentity(assignment.ObjectAPIName) + "\x00" + foldIdentity(*assignment.RecordTypeID)
			if _, ok := recordTypes[recordTypeKey]; !ok {
				return fmt.Errorf("%s references absent record type", path)
			}
		}
	}
	previous = ""
	for index, related := range doc.RelatedLists {
		path := fmt.Sprintf("pageLayoutResolution.relatedLists[%d]", index)
		if err := validatePageLayoutRelatedList(path, related, objects); err != nil {
			return err
		}
		key := pageLayoutRelatedListKey(related)
		if key <= previous {
			return fmt.Errorf("pageLayoutResolution.relatedLists is not canonical")
		}
		previous = key
	}
	return nil
}

func (v *RuntimeMetadataView) SupportsPageLayoutResolutionServing() bool {
	return v != nil && v.doc.Profile == ProjectionPageLayoutResolutionV1 &&
		v.doc.Schema == RuntimeMetadataViewSchemaV23 && v.doc.PageLayoutResolution != nil
}

// PageLayoutResolutionUsesRoleAssignments reports whether resolving the named
// object can depend on role ancestry. Callers may omit a role-hierarchy reader
// only when the immutable view proves that the object has no role dimension.
func (v *RuntimeMetadataView) PageLayoutResolutionUsesRoleAssignments(objectID string) bool {
	if !v.SupportsPageLayoutResolutionServing() {
		return false
	}
	for _, assignment := range v.doc.PageLayoutResolution.Assignments {
		if strings.EqualFold(assignment.ObjectAPIName, objectID) && assignment.RoleID != nil {
			return true
		}
	}
	return false
}

// ValidatePageLayoutContext verifies the Catalog-owned object and RecordType
// identities used by a page request. Layout content and assignment selection
// are intentionally excluded; those are current tenant Runtime state.
func (v *RuntimeMetadataView) ValidatePageLayoutContext(objectID, recordTypeID string) error {
	if !v.SupportsPageLayoutResolutionServing() {
		return ErrIncompatible
	}
	doc := v.doc.PageLayoutResolution
	objectFound := false
	for _, candidate := range doc.ObjectIDs {
		if strings.EqualFold(candidate, objectID) {
			objectFound = true
			break
		}
	}
	if !objectFound {
		return ErrPageLayoutNotFound
	}
	if recordTypeID == "" {
		return nil
	}
	for _, recordType := range doc.RecordTypes {
		if strings.EqualFold(recordType.ObjectAPIName, objectID) &&
			strings.EqualFold(recordType.RecordTypeID, recordTypeID) {
			return nil
		}
	}
	return ErrPageLayoutRecordTypeMissing
}

// ResolvePageLayoutRelatedLists projects only App-release-owned virtual
// related-list behavior for the supplied current database Layout.
func (v *RuntimeMetadataView) ResolvePageLayoutRelatedLists(
	objectID string,
	layout *models.PageLayout,
) ([]RelatedListResolutionInput, error) {
	if err := v.ValidatePageLayoutContext(objectID, ""); err != nil {
		return nil, err
	}
	if layout == nil || !strings.EqualFold(layout.ObjectAPIName, objectID) {
		return nil, ErrPageLayoutNotFound
	}
	doc := v.doc.PageLayoutResolution
	resolutions := make([]RelatedListResolutionInput, 0)
	for _, related := range layout.RelatedLists {
		for _, candidate := range doc.RelatedLists {
			if strings.EqualFold(candidate.ParentObjectID, objectID) &&
				strings.EqualFold(candidate.DisplayObjectID, related.ObjectAPIName) &&
				strings.EqualFold(candidate.DisplayLookupField, related.LookupField) {
				resolutions = append(resolutions, RelatedListResolutionInput{
					ParentObjectID: candidate.ParentObjectID, DisplayObjectID: candidate.DisplayObjectID,
					DisplayLookupField: candidate.DisplayLookupField, Query: candidate.Query,
					Detail: candidate.Detail, ListTarget: candidate.ListTarget, Create: candidate.Create,
				})
				break
			}
		}
	}
	sort.Slice(resolutions, func(i, j int) bool {
		left := foldIdentity(resolutions[i].DisplayObjectID) + "\x00" + foldIdentity(resolutions[i].DisplayLookupField)
		right := foldIdentity(resolutions[j].DisplayObjectID) + "\x00" + foldIdentity(resolutions[j].DisplayLookupField)
		return left < right
	})
	return resolutions, nil
}

// ResolvePageLayout selects one immutable caller-effective layout. roleAncestors
// must contain the caller's own role first, followed by parents in nearest-first
// order; the request wire cannot supply this trusted identity.
func (v *RuntimeMetadataView) ResolvePageLayout(objectID, profileID string, roleAncestors []string, recordTypeID string) (*models.PageLayout, []RelatedListResolutionInput, error) {
	if !v.SupportsPageLayoutResolutionServing() {
		return nil, nil, ErrIncompatible
	}
	doc := v.doc.PageLayoutResolution
	if recordTypeID != "" {
		found := false
		for _, recordType := range doc.RecordTypes {
			if strings.EqualFold(recordType.ObjectAPIName, objectID) && strings.EqualFold(recordType.RecordTypeID, recordTypeID) {
				found = true
				break
			}
		}
		if !found {
			return nil, nil, ErrPageLayoutRecordTypeMissing
		}
	}
	roleDistance := make(map[string]int, len(roleAncestors))
	for index, roleID := range roleAncestors {
		key := foldIdentity(roleID)
		if _, exists := roleDistance[key]; !exists {
			roleDistance[key] = index
		}
	}
	var selected *models.LayoutAssignment
	selectedDistance := 0
	for index := range doc.Assignments {
		candidate := &doc.Assignments[index]
		if !strings.EqualFold(candidate.ObjectAPIName, objectID) || !pageLayoutAssignmentMatches(*candidate, profileID, recordTypeID, roleDistance) {
			continue
		}
		distance := 0
		if candidate.RoleID != nil {
			distance = roleDistance[foldIdentity(*candidate.RoleID)]
		}
		if selected == nil || betterPageLayoutAssignment(*candidate, distance, *selected, selectedDistance) {
			selected, selectedDistance = candidate, distance
		}
	}
	if selected == nil {
		return nil, nil, ErrPageLayoutNotFound
	}
	var layout *models.PageLayout
	for index := range doc.Layouts {
		if strings.EqualFold(doc.Layouts[index].ID, selected.LayoutID) {
			cloned, err := cloneJSON(doc.Layouts[index])
			if err != nil {
				return nil, nil, err
			}
			layout = &cloned
			break
		}
	}
	if layout == nil {
		return nil, nil, ErrPageLayoutNotFound
	}
	resolutions := make([]RelatedListResolutionInput, 0)
	for _, related := range layout.RelatedLists {
		for _, candidate := range doc.RelatedLists {
			if strings.EqualFold(candidate.ParentObjectID, objectID) && strings.EqualFold(candidate.DisplayObjectID, related.ObjectAPIName) && strings.EqualFold(candidate.DisplayLookupField, related.LookupField) {
				resolutions = append(resolutions, RelatedListResolutionInput{
					ParentObjectID: candidate.ParentObjectID, DisplayObjectID: candidate.DisplayObjectID,
					DisplayLookupField: candidate.DisplayLookupField, Query: candidate.Query,
					Detail: candidate.Detail, ListTarget: candidate.ListTarget, Create: candidate.Create,
				})
				break
			}
		}
	}
	sort.Slice(resolutions, func(i, j int) bool {
		left := foldIdentity(resolutions[i].DisplayObjectID) + "\x00" + foldIdentity(resolutions[i].DisplayLookupField)
		right := foldIdentity(resolutions[j].DisplayObjectID) + "\x00" + foldIdentity(resolutions[j].DisplayLookupField)
		return left < right
	})
	return layout, resolutions, nil
}

func pageLayoutAssignmentMatches(value models.LayoutAssignment, profileID, recordTypeID string, roles map[string]int) bool {
	if value.ProfileID != nil && !strings.EqualFold(*value.ProfileID, profileID) {
		return false
	}
	if value.RecordTypeID != nil && !strings.EqualFold(*value.RecordTypeID, recordTypeID) {
		return false
	}
	if value.RoleID != nil {
		_, ok := roles[foldIdentity(*value.RoleID)]
		return ok
	}
	return true
}

func betterPageLayoutAssignment(left models.LayoutAssignment, leftDistance int, right models.LayoutAssignment, rightDistance int) bool {
	if left.Specificity != right.Specificity {
		return left.Specificity > right.Specificity
	}
	if (left.RecordTypeID != nil) != (right.RecordTypeID != nil) {
		return left.RecordTypeID != nil
	}
	if (left.ProfileID != nil) != (right.ProfileID != nil) {
		return left.ProfileID != nil
	}
	if (left.RoleID != nil) != (right.RoleID != nil) {
		return left.RoleID != nil
	}
	if left.RoleID != nil && leftDistance != rightDistance {
		return leftDistance < rightDistance
	}
	return identityLess(left.ID, right.ID)
}
