package rest

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	metasvc "github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/mru"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/fieldcapabilities"
	"github.com/nexuscrm/shared/pkg/fieldnormalize"
	"github.com/nexuscrm/shared/pkg/fieldprovenance"
	"github.com/nexuscrm/shared/pkg/models"
)

func uniqueRelationshipsByField(relationships []*models.Relationship) map[string]*models.Relationship {
	byField := make(map[string]*models.Relationship, len(relationships))
	duplicates := make(map[string]bool)
	for _, rel := range relationships {
		if rel == nil || rel.FieldAPIName == "" {
			continue
		}
		key := strings.ToLower(rel.FieldAPIName)
		if _, exists := byField[key]; exists {
			duplicates[key] = true
			delete(byField, key)
			continue
		}
		if duplicates[key] {
			continue
		}
		byField[key] = rel
	}
	return byField
}

func relationshipsByParentAndField(relationships []*models.Relationship) map[string]*models.Relationship {
	byKey := make(map[string]*models.Relationship, len(relationships))
	for _, rel := range relationships {
		if rel == nil || rel.ParentObjectAPIName == "" || rel.FieldAPIName == "" {
			continue
		}
		key := strings.ToLower(rel.ParentObjectAPIName) + "|" + strings.ToLower(rel.FieldAPIName)
		byKey[key] = rel
	}
	return byKey
}

func relationshipDeleteSemantics(field models.FieldMetadata, rel *models.Relationship) (bool, bool) {
	if rel != nil {
		return rel.CascadeDelete, rel.RestrictedDelete
	}

	cascadeDelete := field.IsMasterDetail || field.Type == constants.FieldTypeMasterDetail
	restrictedDelete := false
	if field.DeleteRule != nil {
		restrictedDelete = strings.EqualFold(string(*field.DeleteRule), string(constants.DeleteRuleRestrict))
	}
	return cascadeDelete, restrictedDelete
}

// ListObjects handles GET /services/data/vXX.X/sobjects
func (h *SObjectHandler) ListObjects(c *gin.Context) {
	metadata := h.svcMgr.Metadata.GetSchemas(c.Request.Context())
	user := MustGetUser(c)
	sobjects := make([]DescribeGlobalObject, 0)

	for _, m := range metadata {
		canRead := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), m.APIName, constants.PermRead, user)
		if !canRead {
			continue
		}

		canCreate := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), m.APIName, constants.PermCreate, user)
		canEdit := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), m.APIName, constants.PermEdit, user)
		canDelete := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), m.APIName, constants.PermDelete, user)

		dto := DescribeGlobalObject{
			Activateable:          false,
			AssociateEntityType:   nil,
			AssociateParentEntity: nil,
			Createable:            canCreate,
			Custom:                m.IsCustom,
			CustomSetting:         false,
			Deletable:             canDelete,
			DeprecatedAndHidden:   false,
			FeedEnabled:           true,
			HasSubtypes:           false,
			IsSubtype:             false,
			KeyPrefix:             mru.GetKeyPrefix(m.APIName),
			Label:                 m.Label,
			LabelPlural:           m.PluralLabel,
			Layoutable:            !m.IsSystem,
			Mergeable:             false,
			MruEnabled:            true,
			Name:                  m.APIName,
			IsSystem:              m.IsSystem,
			Queryable:             canRead,
			Replicateable:         canRead,
			Retrieveable:          canRead,
			Searchable:            canRead && m.Searchable,
			TableType:             string(m.TableType),
			Triggerable:           !m.IsSystem,
			Undeletable:           true,
			Updateable:            canEdit,
			Urls: map[string]string{
				"sobject":     fmt.Sprintf("/services/data/v63.0/sobjects/%s", m.APIName),
				"describe":    fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe", m.APIName),
				"rowTemplate": fmt.Sprintf("/services/data/v63.0/sobjects/%s/{ID}", m.APIName),
			},
		}
		sobjects = append(sobjects, dto)
	}

	response := DescribeGlobalResult{
		Encoding:     "UTF-8",
		MaxBatchSize: 200,
		SObjects:     sobjects,
	}

	c.JSON(http.StatusOK, response)
}

// DescribeObject handles GET /services/data/vXX.X/sobjects/:sobject/describe
func (h *SObjectHandler) DescribeObject(c *gin.Context) {
	objectName := c.Param("sobject")
	user := MustGetUser(c)
	canRead := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), objectName, constants.PermRead, user)
	if !canRead {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, "Insufficient access to describe this object"))
		return
	}

	schema := h.svcMgr.Metadata.GetSchema(c.Request.Context(), objectName)
	if schema == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "The requested resource does not exist"))
		return
	}

	canCreate := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), objectName, constants.PermCreate, user)
	canEdit := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), objectName, constants.PermEdit, user)
	canDelete := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), objectName, constants.PermDelete, user)

	resolver := permissions.NewFieldAccessResolver(h.svcMgr.Permissions)
	relationshipsByField := uniqueRelationshipsByField(h.svcMgr.Metadata.GetRelationships(c.Request.Context(), objectName))
	// Reverse index: member field APIName (lowercased) → parent compound APIName.
	// Salesforce describe emits `compoundFieldName` on members of compound
	// parents. Compound declarations are derived from L1 manifests by
	// fieldprovenance.ClassifyCompound; the struct fields f.IsCompound /
	// f.ComposedFrom are not populated on DB-loaded schemas under v1.6,
	// so we must consult the classifier rather than trust the struct.
	compoundParentByMember := make(map[string]string)
	for _, cf := range schema.Fields {
		isCompound, composedFrom := fieldprovenance.ClassifyCompound(objectName, cf.APIName)
		if !isCompound {
			continue
		}
		for _, member := range composedFrom {
			compoundParentByMember[strings.ToLower(member)] = cf.APIName
		}
	}
	fields := make([]SObjectField, 0)
	for _, f := range schema.Fields {
		normalizedType := constants.NormalizeFieldType(string(f.Type))
		rel := relationshipsByField[strings.ToLower(f.APIName)]
		cascadeDelete, restrictedDelete := relationshipDeleteSemantics(f, rel)
		runtimePicklist := buildRuntimePicklistInfo(c.Request.Context(), h.svcMgr.Metadata, f)

		sfType := mapFieldTypeToSF(f)
		var compoundFieldName *string
		if parent, ok := compoundParentByMember[strings.ToLower(f.APIName)]; ok {
			compoundFieldName = &parent
		}
		var typedDefault interface{}
		if f.DefaultValue != nil {
			typedDefault = fieldnormalize.NormalizeDDLDefault(*f.DefaultValue, string(f.Type))
		}
		dto := SObjectField{
			Aggregatable:                 isAggregatable(sfType),
			AiPredictionField:            false,
			AutoNumber:                   normalizedType == constants.FieldTypeAutonumber,
			ByteLength:                   fieldLength(f),
			Calculated:                   normalizedType == constants.FieldTypeFormula,
			CalculatedFormula:            f.Formula,
			CascadeDelete:                cascadeDelete,
			CaseSensitive:                false,
			CompoundFieldName:            compoundFieldName,
			Createable:                   resolver.EffectiveCreateable(c.Request.Context(), &f, objectName, user),
			Custom:                       !f.IsSystem,
			DefaultValue:                 typedDefault,
			DefaultedOnCreate:            f.DefaultValue != nil,
			DependentPicklist:            runtimePicklist != nil,
			DeprecatedAndHidden:          strings.Contains(strings.ToLower(f.Label), "(do not use)"),
			Digits:                       fieldDigits(f),
			DisplayLocationInDecimal:     sfType == "location",
			Encrypted:                    false,
			ExternalID:                   f.IsUnique,
			Filterable:                   isFilterable(sfType),
			FilteredLookupInfo:           nil,
			FormulaTreatNullNumberAsZero: false,
			Groupable:                    isGroupable(sfType),
			HighScaleNumber:              false,
			HtmlFormatted:                f.Type == constants.FieldTypeRichtextarea,
			IdLookup:                     f.IsUnique || f.APIName == "Id",
			Label:                        f.Label,
			Length:                       fieldLength(f),
			Name:                         f.APIName,
			NameField:                    f.IsNameField,
			Nillable:                     isNillable(sfType, f.Required),
			Permissionable:               true,
			PicklistValues:               mapPicklistValues(f),
			PolymorphicForeignKey:        false,
			Precision:                    fieldPrecision(f),
			QueryByDistance:              sfType == "location",
			Readable:                     h.svcMgr.Permissions.CheckFieldVisibilityWithUser(c.Request.Context(), objectName, f.APIName, user),
			ReferenceTo:                  nonNilStringSlice(f.ReferenceTo),
			RelationshipName:             f.RelationshipName,
			RestrictedDelete:             restrictedDelete,
			RestrictedPicklist:           false,
			Scale:                        fieldScale(f),
			SearchPrefilterable:          true,
			SoapType:                     mapFieldTypeToSoapType(models.FieldType(sfType)),
			Sortable:                     isSortable(sfType),
			Type:                         sfType,
			Unique:                       f.IsUnique,
			Updateable:                   resolver.EffectiveUpdateable(c.Request.Context(), &f, objectName, user),
		}
		// Virtual-field capability overrides. The generic per-type helpers
		// above describe what the query engine could do with a physical
		// boolean column; virtual fields have no physical column and the
		// first version of the permission refactor does not rewrite
		// WHERE/ORDER BY/GROUP BY clauses into junction joins. Declaring
		// those capabilities true would make describe lie about the query
		// engine. SystemPermission virtuals also share object-level CRUD
		// gating instead of per-field FLS (there is no FieldPermissions
		// row for a projection), so Createable/Updateable/Readable come
		// from the PermissionSet object permissions rather than the
		// per-field FLS resolver.
		if f.IsVirtual {
			dto.Filterable = false
			dto.Sortable = false
			dto.Groupable = false
			dto.Aggregatable = false
			if f.VirtualKind == constants.VirtualFieldSystemPermission {
				dto.Createable = canCreate
				dto.Updateable = canEdit
				dto.Readable = canRead
				dto.DefaultValue = false
				dto.DefaultedOnCreate = true
				dto.Custom = false
			}
		}
		if runtimePicklist != nil {
			dto.ControllerName = f.ControllingField
			dto.FilteredLookupInfo = FilteredLookupInfo{RuntimePicklist: runtimePicklist}
		}
		fields = append(fields, dto)
	}

	// Build childRelationships
	childRelationships := make([]ChildRelationship, 0)
	allSchemas := h.svcMgr.Metadata.GetSchemas(c.Request.Context())
	for _, childSchema := range allSchemas {
		childRelationshipsByField := relationshipsByParentAndField(h.svcMgr.Metadata.GetRelationships(c.Request.Context(), childSchema.APIName))
		for _, f := range childSchema.Fields {
			isRef := f.Type == constants.FieldTypeReference || f.Type == constants.FieldTypeMasterDetail
			if !isRef || f.RelationshipName == nil || *f.RelationshipName == "" {
				continue
			}
			for _, ref := range f.ReferenceTo {
				if strings.EqualFold(ref, objectName) {
					rel := childRelationshipsByField[strings.ToLower(objectName)+"|"+strings.ToLower(f.APIName)]
					cascadeDelete, restrictedDelete := relationshipDeleteSemantics(f, rel)
					cr := ChildRelationship{
						CascadeDelete:       cascadeDelete,
						ChildSObject:        childSchema.APIName,
						DeprecatedAndHidden: false,
						Field:               f.APIName,
						JunctionIdListNames: nonNilStringSlice(nil),
						JunctionReferenceTo: nonNilStringSlice(nil),
						RelationshipName:    *f.RelationshipName,
						RestrictedDelete:    restrictedDelete,
					}
					childRelationships = append(childRelationships, cr)
					break
				}
			}
		}
	}

	recordTypeInfos := []RecordTypeInfo{
		{
			Active:                   true,
			Available:                true,
			DefaultRecordTypeMapping: true,
			DeveloperName:            "Master",
			Master:                   true,
			Name:                     "Master",
			RecordTypeId:             "012000000000000AAA",
			Urls: map[string]string{
				"layout": fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/layouts/012000000000000AAA", schema.APIName),
			},
		},
	}

	// Append real record types from database
	dbRecordTypes := h.svcMgr.Metadata.GetRecordTypes(c.Request.Context(), objectName)
	if len(dbRecordTypes) > 0 {
		// Master is no longer the default when real types exist
		recordTypeInfos[0].DefaultRecordTypeMapping = false
		defaultAssigned := false
		for _, rt := range dbRecordTypes {
			if !rt.IsActive {
				continue
			}
			isDefault := !defaultAssigned
			if isDefault {
				defaultAssigned = true
			}
			recordTypeInfos = append(recordTypeInfos, RecordTypeInfo{
				Active:                   true,
				Available:                true,
				DefaultRecordTypeMapping: isDefault,
				DeveloperName:            strings.ReplaceAll(rt.Name, " ", "_"),
				Master:                   false,
				Name:                     rt.Name,
				RecordTypeId:             rt.ID,
				Urls: map[string]string{
					"layout": fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/layouts/%s", schema.APIName, rt.ID),
				},
			})
		}
	}

	pathField := ""
	if schema.PathField != nil {
		pathField = *schema.PathField
	}

	response := DescribeSObjectResult{
		ActionOverrides:       []interface{}{},
		Activateable:          false,
		AssociateEntityType:   nil,
		AssociateParentEntity: nil,
		ChildRelationships:    childRelationships,
		CompactLayoutable:     true,
		Createable:            canCreate,
		Custom:                schema.IsCustom,
		CustomSetting:         false,
		Deletable:             canDelete,
		DeprecatedAndHidden:   false,
		FeedEnabled:           true,
		Fields:                fields,
		HasSubtypes:           false,
		IsSubtype:             false,
		KeyPrefix:             mru.GetKeyPrefix(schema.APIName),
		Label:                 schema.Label,
		LabelPlural:           schema.PluralLabel,
		Layoutable:            !schema.IsSystem,
		ListFields:            schema.ListFields,
		PathField:             pathField,
		Mergeable:             false,
		MruEnabled:            true,
		Name:                  schema.APIName,
		NamedLayoutInfos:      []interface{}{},
		Queryable:             canRead,
		RecordTypeInfos:       recordTypeInfos,
		Replicateable:         canRead,
		Retrieveable:          canRead,
		SearchLayoutable:      true,
		Searchable:            canRead && schema.Searchable,
		SupportedScopes: []interface{}{
			map[string]string{"label": fmt.Sprintf("All %s", schema.PluralLabel), "name": "everything"},
			map[string]string{"label": fmt.Sprintf("My %s", schema.PluralLabel), "name": "mine"},
		},
		Triggerable: !schema.IsSystem,
		Undeletable: true,
		Updateable:  canEdit,
		Urls: map[string]string{
			"sobject":         fmt.Sprintf("/services/data/v63.0/sobjects/%s", schema.APIName),
			"describe":        fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe", schema.APIName),
			"rowTemplate":     fmt.Sprintf("/services/data/v63.0/sobjects/%s/{ID}", schema.APIName),
			"layouts":         fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/layouts", schema.APIName),
			"compactLayouts":  fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/compactLayouts", schema.APIName),
			"approvalLayouts": fmt.Sprintf("/services/data/v63.0/sobjects/%s/describe/approvalLayouts", schema.APIName),
			"quickActions":    fmt.Sprintf("/services/data/v63.0/sobjects/%s/quickActions", schema.APIName),
			"listviews":       fmt.Sprintf("/services/data/v63.0/sobjects/%s/listviews", schema.APIName),
			"defaultValues":   fmt.Sprintf("/services/data/v63.0/sobjects/%s/defaultValues", schema.APIName),
		},
	}

	c.JSON(http.StatusOK, response)
}

// mapFieldTypeToSoapType helper
func mapFieldTypeToSoapType(t models.FieldType) string {
	switch constants.SchemaFieldType(t) {
	case constants.FieldTypeString, constants.FieldTypeTextarea, constants.FieldTypeRichtextarea, constants.FieldTypePhone, constants.FieldTypeEmail, constants.FieldTypeUrl, constants.FieldTypePicklist, constants.FieldTypeMultiPicklist, constants.FieldTypeEncryptedString, constants.FieldTypeAutonumber:
		return "xsd:string"
	case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
		return "xsd:double"
	case constants.FieldTypeBoolean:
		return "xsd:boolean"
	case constants.FieldTypeDate:
		return "xsd:date"
	case constants.FieldTypeDatetime:
		return "xsd:dateTime"
	case constants.FieldTypeReference, constants.FieldTypeMasterDetail:
		return "tns:ID"
	default:
		return "xsd:string"
	}
}

// DescribeLayouts handles GET /services/data/vXX.X/sobjects/:sobject/describe/layouts
// Salesforce parity: Returns layouts: null + populated recordTypeMappings.
// The actual layout content is served by DescribeLayoutByRecordType.
func (h *SObjectHandler) DescribeLayouts(c *gin.Context) {
	objectName := c.Param("sobject")
	user := MustGetUser(c)
	canRead := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), objectName, constants.PermRead, user)
	if !canRead {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, "Insufficient access to describe layouts"))
		return
	}

	schema := h.svcMgr.Metadata.GetSchema(c.Request.Context(), objectName)
	if schema == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "The requested resource does not exist"))
		return
	}

	masterRecordTypeID := "012000000000000AAA"
	layoutID := fmt.Sprintf("%s-default-layout", objectName)

	mappings := []gin.H{
		{
			"active":                   true,
			"available":                true,
			"defaultRecordTypeMapping": true,
			"developerName":            "Master",
			"layoutId":                 layoutID,
			"master":                   true,
			"name":                     "Master",
			"recordTypeId":             masterRecordTypeID,
			"urls":                     gin.H{},
		},
	}

	// Append active record types from database (same pattern as DescribeObject)
	dbRecordTypes := h.svcMgr.Metadata.GetRecordTypes(c.Request.Context(), objectName)
	activeCount := 0
	defaultAssigned := false
	for _, rt := range dbRecordTypes {
		if !rt.IsActive {
			continue
		}
		activeCount++
		isDefault := !defaultAssigned
		if isDefault {
			defaultAssigned = true
		}
		mappings = append(mappings, gin.H{
			"active":                   true,
			"available":                true,
			"defaultRecordTypeMapping": isDefault,
			"developerName":            strings.ReplaceAll(rt.Name, " ", "_"),
			"layoutId":                 layoutID,
			"master":                   false,
			"name":                     rt.Name,
			"recordTypeId":             rt.ID,
			"urls":                     gin.H{},
		})
	}
	if activeCount > 0 {
		mappings[0]["defaultRecordTypeMapping"] = false
	}

	c.JSON(http.StatusOK, gin.H{
		"layouts":                    nil,
		"recordTypeMappings":         mappings,
		"recordTypeSelectorRequired": activeCount > 0,
	})
}

// DescribeLayoutByRecordType handles GET /services/data/vXX.X/sobjects/:sobject/describe/layouts/:recordTypeId
// Salesforce parity: Returns the full layout object with sections and relatedLists.
func (h *SObjectHandler) DescribeLayoutByRecordType(c *gin.Context) {
	objectName := c.Param("sobject")
	user := MustGetUser(c)
	canRead := h.svcMgr.Permissions.CheckObjectPermissionWithUser(c.Request.Context(), objectName, constants.PermRead, user)
	if !canRead {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, "Insufficient access to describe layouts"))
		return
	}

	schema := h.svcMgr.Metadata.GetSchema(c.Request.Context(), objectName)
	if schema == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "The requested resource does not exist"))
		return
	}

	// Resolve layout using the new specificity-based resolution.
	// EC-6: Pass recordTypeId from URL param (was previously ignored).
	profileID := user.ProfileID
	recordTypeID := c.Param("recordTypeId")

	// Compute role ancestors for resolution
	var roleAncestors []string
	if user.RoleID != nil {
		roleAncestors = append([]string{*user.RoleID}, h.svcMgr.Permissions.GetRoleAncestors(*user.RoleID)...)
	}

	storedLayout := h.svcMgr.Metadata.ResolveLayout(c.Request.Context(), objectName, roleAncestors, profileID, recordTypeID)
	if storedLayout == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, fmt.Sprintf("No layout could be resolved for object %s", objectName)))
		return
	}

	// Use the layout's curated related lists (populated by ResolveLayout/augment),
	// not raw auto-discovery. This matches Salesforce behavior where Quick Links
	// and record pages show only layout-configured related lists.
	relatedLists := storedLayoutRelatedListsToSFFormat(storedLayout.RelatedLists)

	c.JSON(http.StatusOK, convertStoredLayoutToSFFormat(storedLayout, schema, relatedLists))
}

// convertStoredLayoutToSFFormat converts a stored PageLayout into the Salesforce-compatible
// JSON response format used by DescribeLayoutByRecordType.
func convertStoredLayoutToSFFormat(layout *models.PageLayout, schema *models.ObjectMetadata, relatedLists []gin.H) gin.H {
	// Build a field metadata lookup for the schema
	fieldMap := make(map[string]models.FieldMetadata, len(schema.Fields))
	for _, f := range schema.Fields {
		fieldMap[f.APIName] = f
	}

	// Build SF-format sections from stored layout sections
	sfDetailSections := make([]gin.H, 0, len(layout.Sections))
	sfSections := make([]gin.H, 0, len(layout.Sections))
	for _, sec := range layout.Sections {
		layoutItems := make([]map[string]interface{}, 0, len(sec.Fields))
		for _, fieldName := range sec.Fields {
			f, ok := fieldMap[fieldName]
			if !ok {
				continue
			}
			// Layout projection has no user context (transformLayout-style),
			// so read intrinsic capability directly from the live authority
			// rather than the stored cache. Keeps legacy-discovered /
			// compound fields non-editable immediately after
			// system_tables.json changes, without waiting for a
			// BackfillFieldCapabilities pass. Issue #1582 RFC v1.5 PR D.
			fCopy := f
			createable, updateable := fieldcapabilities.ComputeIntrinsic(schema.APIName, &fCopy)
			layoutItems = append(layoutItems, map[string]interface{}{
				"editableForNew":    createable,
				"editableForUpdate": updateable,
				"field":             f.APIName,
				"label":             f.Label,
				"required":          f.Required,
			})
		}
		sfDetailSections = append(sfDetailSections, gin.H{
			"heading":    sec.Label,
			"columns":    sec.Columns,
			"rows":       len(layoutItems),
			"useHeading": true,
			"layoutRows": []gin.H{
				{"layoutItems": layoutItems},
			},
		})
		sfSections = append(sfSections, gin.H{
			"id":      sec.ID,
			"label":   sec.Label,
			"columns": sec.Columns,
			"fields":  sec.Fields,
		})
	}

	return gin.H{
		"id":                   layout.ID,
		"name":                 layout.LayoutName,
		"detailLayoutSections": sfDetailSections,
		"editLayoutSections":   sfDetailSections,
		"relatedLists":         relatedLists,
		"sections":             sfSections,
	}
}

// storedLayoutRelatedListsToSFFormat converts stored RelatedListConfig items
// into the Salesforce-compatible wire format used by describe/layouts responses.
func storedLayoutRelatedListsToSFFormat(rls []models.RelatedListConfig) []gin.H {
	result := make([]gin.H, 0, len(rls))
	for _, rl := range rls {
		entry := gin.H{
			"accessLevel": nil,
			"buttons":     []interface{}{},
			"columns": []gin.H{
				{"field": "name", "format": nil, "label": "Name", "lookupId": "id", "name": "name", "sortable": true, "type": "url"},
			},
			"custom":         false,
			"field":          rl.LookupField,
			"label":          rl.Label,
			"secondaryLabel": rl.SecondaryLabel,
			"limitRows":      5,
			"name":           rl.Label,
			"sobject":        rl.ObjectAPIName,
			"sort":           []interface{}{},
		}
		if res := metasvc.ResolveVirtualRelatedList(rl.ObjectAPIName, rl.LookupField); res != nil {
			entry["resolution"] = res
		}
		result = append(result, entry)
	}
	return result
}

// GetRelatedListCandidates handles GET /ui-api/related-list-candidates/:objectApiName
// Returns structured candidates for the layout editor's related list picker.
func (h *SObjectHandler) GetRelatedListCandidates(c *gin.Context) {
	objectName := c.Param("objectApiName")

	schema := h.svcMgr.Metadata.GetSchema(c.Request.Context(), objectName)
	if schema == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Object not found"))
		return
	}

	candidates, err := h.svcMgr.Metadata.GetRelatedListCandidates(c.Request.Context(), objectName)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to get related list candidates"))
		return
	}

	c.JSON(http.StatusOK, gin.H{"candidates": candidates})
}
