package metadata

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"sort"
	"strings"

	"github.com/nexuscrm/backend/internal/domain/ports"
	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// currentGenerationVersion is bumped when the smart layout algorithm improves.
// Auto-generated layouts (Source="auto_generated") with a lower GenerationVersion
// are regenerated in-place on next access. Admin-created layouts are never touched.
// v4: add Activity tab to all standard object layouts (Ref #183)
// v5: standardObjectSections — Salesforce-parity section names for 9 standard objects
//   - extra sections (Campaign Planning, Quote Pricing/Prepared For, etc.)
//
// v6: exclude Password + security-internal fields (FailedLoginAttempts, LockedUntil, LastLoginDate) from layouts
// v7: add Invoice Collections to auto-generated Order related lists
// v8: include Order archived order form fields after #610
const currentGenerationVersion = 8

// currentLayoutVersion is kept as an alias for backward compatibility during migration.
// DEPRECATED: use currentGenerationVersion instead.
const currentLayoutVersion = currentGenerationVersion

// minPrefixGroupSize is the minimum number of custom fields sharing a prefix
// required to form a dedicated section. Groups below this threshold remain in
// "Additional Details".
const minPrefixGroupSize = 3

const campaignHierarchyLabel = "Campaign Hierarchy"

var campaignHierarchyFields = []string{
	constants.FieldName,
	"ActualCost",
	"AmountAllOpportunities",
	"AmountWonOpportunities",
}

func normalizeAssignmentRouteSource(routeSource *string) *string {
	if routeSource == nil {
		return nil
	}
	trimmed := strings.TrimSpace(*routeSource)
	if trimmed == "" {
		return nil
	}
	return &trimmed
}

// --- Layout Methods ---

// GetLayout is a backward-compatible wrapper around ResolveLayout.
// It converts the old (apiName, *profileID) signature to the new multi-dimensional resolution.
// Callers should migrate to ResolveLayout for full Role + RecordType support.
func (ms *MetadataService) GetLayout(ctx context.Context, apiName string, profileID *string) *models.PageLayout {
	pid := ""
	if profileID != nil {
		pid = *profileID
	}
	return ms.ResolveLayout(ctx, apiName, nil, pid, "")
}

// ResolveLayout resolves the best layout for an object given user context.
// Uses specificity-based assignment lookup, with auto-generation as initialization fallback.
// Always resolves the single default layout — mode (View/Edit/Create) is a response-time
// concern handled by the service layer, not a resolution concern.
func (ms *MetadataService) ResolveLayout(ctx context.Context, apiName string, roleAncestors []string, profileID string, recordTypeID string) *models.PageLayout {
	// Ensure cache is initialized for schema fallback (virtual objects)
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("⚠️ Failed to initialize core cache in ResolveLayout: %v", err)
	}

	ms.mu.RLock()
	defer ms.mu.RUnlock()

	// 1. Find best assignment via specificity-based resolution
	assignment, err := ms.repo.FindBestAssignment(ctx, apiName, roleAncestors, profileID, recordTypeID)
	if err != nil {
		log.Printf("⚠️ FindBestAssignment failed for %s: %v", apiName, err)
	}

	var layout *models.PageLayout
	needsDefaultAssignmentRepair := false

	if assignment != nil {
		layout, err = ms.repo.GetLayout(ctx, assignment.LayoutID)
		if err != nil {
			log.Printf("⚠️ GetLayout failed for assignment %s -> layout %s: %v", assignment.ID, assignment.LayoutID, err)
		}
		// EC-1: Assignment points to soft-deleted layout — clean up and fall through
		if layout == nil {
			log.Printf("⚠️ Layout %s referenced by assignment %s is deleted/missing, removing orphaned assignment", assignment.LayoutID, assignment.ID)
			_ = ms.repo.DeleteAssignment(ctx, assignment.ID)
			assignment = nil
			needsDefaultAssignmentRepair = true
		}
	}

	// 2. No assignment or orphaned assignment — prefer an existing persisted layout
	// before generating a new synthetic one. This keeps layout IDs stable and ensures
	// any repaired wildcard assignment points at a real _System_PageLayout row.
	if layout == nil {
		layouts, lErr := ms.repo.GetLayouts(ctx, apiName)
		if lErr != nil {
			log.Printf("⚠️ GetLayouts failed for %s: %v", apiName, lErr)
		} else if len(layouts) > 0 && layouts[0] != nil {
			layout = layouts[0]
			needsDefaultAssignmentRepair = true
		}
	}

	// 3. Check if auto-generated layout needs regeneration (Decision 7: in-place UPDATE)
	if layout != nil && layout.Source == "auto_generated" && layout.GenerationVersion < currentGenerationVersion {
		obj := ms.resolveSchema(ctx, apiName)
		if obj != nil {
			newLayout := generateSmartLayout(apiName, obj)
			newLayout.RelatedLists = ms.seedRelatedLists(apiName)
			newLayout.ID = layout.ID // preserve ID — all FK references intact
			newLayout.IsDefault = layout.IsDefault
			newLayout.Source = "auto_generated"
			newLayout.GenerationVersion = currentGenerationVersion
			// Preserve deprecated fields for backward compat
			newLayout.IsCustomized = false
			newLayout.LayoutVersion = currentGenerationVersion
			if uErr := ms.repo.UpsertLayout(ctx, newLayout); uErr != nil {
				log.Printf("⚠️ Failed to regenerate layout %s (v%d→v%d): %v", layout.ID, layout.GenerationVersion, currentGenerationVersion, uErr)
				// Return stale layout rather than nothing (graceful degradation)
			} else {
				ms.writeHistory(ctx, newLayout.ID, fmt.Sprintf("auto_regenerate_v%d", currentGenerationVersion), nil)
				layout = newLayout
			}
		}
	}

	// 4. No assignment or layout found — auto-generate and create default assignment
	if layout == nil {
		obj := ms.resolveSchema(ctx, apiName)
		// EC-2: Schema not found — return nil gracefully
		if obj == nil {
			return nil
		}

		layout = generateSmartLayout(apiName, obj)
		layout.RelatedLists = ms.seedRelatedLists(apiName)
		layout.IsDefault = true
		layout.Source = "auto_generated"
		layout.GenerationVersion = currentGenerationVersion
		// Set deprecated fields for backward compat
		layout.IsCustomized = false
		layout.LayoutVersion = currentGenerationVersion

		if uErr := ms.repo.UpsertLayout(ctx, layout); uErr != nil {
			log.Printf("⚠️ Failed to persist generated layout for %s: %v", apiName, uErr)
		} else {
			needsDefaultAssignmentRepair = true
			ms.writeHistory(ctx, layout.ID, "auto_generate", nil)
		}
	}

	if layout != nil && (assignment == nil || needsDefaultAssignmentRepair) {
		routeSource := models.LayoutAssignmentRouteSourceRuntimeFallback
		defaultAssignment := &models.LayoutAssignment{
			ObjectAPIName: apiName,
			LayoutID:      layout.ID,
			RouteSource:   &routeSource,
		}
		defaultAssignment.ComputeSpecificity()
		if aErr := ms.repo.CreateAssignment(ctx, defaultAssignment); aErr != nil {
			log.Printf("⚠️ Failed to create default assignment for %s: %v", apiName, aErr)
		}
	}

	// Related lists are populated at bootstrap time by BackfillLayoutRelatedLists.
	// Runtime repair only fixes stale persisted entries (labels, fields, campaign hierarchy)
	// — it does NOT add new relationships.
	if layout != nil {
		ms.repairLayoutRelatedLists(ctx, layout)
	}

	return layout
}

// resolveSchema finds the object schema from cache or DB.
func (ms *MetadataService) resolveSchema(ctx context.Context, apiName string) *models.ObjectMetadata {
	snap := ms.loadCoreCache()
	if snap != nil {
		if s, ok := snap.schemaMap[strings.ToLower(apiName)]; ok {
			return s
		}
	}
	s, err := ms.repo.GetSchemaByAPIName(ctx, apiName)
	if err == nil && s != nil {
		return s
	}
	return nil
}

// WriteLayoutHistory appends a version snapshot to the layout history table (public wrapper).
func (ms *MetadataService) WriteLayoutHistory(ctx context.Context, layoutID string, reason string, changedByID *string) {
	ms.writeHistory(ctx, layoutID, reason, changedByID)
}

// RestoreLayout restores a layout to a previous version from history.
func (ms *MetadataService) RestoreLayout(ctx context.Context, layoutID string, versionNumber int) error {
	// Get the historical config snapshot
	configJSON, err := ms.repo.GetLayoutHistoryConfig(ctx, layoutID, versionNumber)
	if err != nil {
		return fmt.Errorf("failed to get history version %d for layout %s: %w", versionNumber, layoutID, err)
	}

	// Unmarshal into a layout to update
	var restoredLayout models.PageLayout
	if err := json.Unmarshal([]byte(configJSON), &restoredLayout); err != nil {
		return fmt.Errorf("failed to unmarshal history config: %w", err)
	}

	// Get current layout to preserve metadata fields
	current, err := ms.repo.GetLayout(ctx, layoutID)
	if err != nil || current == nil {
		return fmt.Errorf("layout %s not found", layoutID)
	}

	// Restore content but keep metadata
	current.Sections = restoredLayout.Sections
	current.CompactLayout = restoredLayout.CompactLayout
	current.RelatedLists = restoredLayout.RelatedLists
	current.HeaderActions = restoredLayout.HeaderActions
	current.QuickActions = restoredLayout.QuickActions
	current.Tabs = restoredLayout.Tabs

	if err := ms.repo.UpsertLayout(ctx, current); err != nil {
		return fmt.Errorf("failed to save restored layout: %w", err)
	}

	// Write history entry for the restore action
	ms.writeHistory(ctx, layoutID, fmt.Sprintf("restored_from_v%d", versionNumber), nil)

	return nil
}

// writeHistory appends a version snapshot to the layout history table.
// Version numbering is atomic (INSERT...SELECT MAX()+1 in the repo layer).
func (ms *MetadataService) writeHistory(ctx context.Context, layoutID string, reason string, changedByID *string) {
	// Get current layout config for snapshot
	layout, err := ms.repo.GetLayout(ctx, layoutID)
	if err != nil || layout == nil {
		return
	}
	configJSON, err := json.Marshal(layout)
	if err != nil {
		return
	}

	entry := &models.PageLayoutHistoryEntry{
		LayoutID:     layoutID,
		Config:       string(configJSON),
		ChangedByID:  changedByID,
		ChangeReason: reason,
		// VersionNumber computed atomically by repo (COALESCE(MAX(VersionNumber),0)+1)
	}
	if hErr := ms.repo.WriteLayoutHistory(ctx, entry); hErr != nil {
		log.Printf("⚠️ Failed to write layout history for %s: %v", layoutID, hErr)
	}

	// Retention pruning: keep only last 20 versions
	const maxHistoryVersions = 20
	maxVer, _ := ms.repo.GetLayoutHistoryMaxVersion(ctx, layoutID)
	if maxVer > maxHistoryVersions {
		pruneBelow := maxVer - maxHistoryVersions
		if pErr := ms.repo.PruneLayoutHistory(ctx, layoutID, pruneBelow); pErr != nil {
			log.Printf("⚠️ Failed to prune layout history for %s: %v", layoutID, pErr)
		}
	}
}

// seedRelatedLists generates an initial set of related lists for a new layout.
// Called at layout creation time so new objects start with populated related lists
// instead of relying on a runtime fallback or waiting for the next startup backfill.
func (ms *MetadataService) seedRelatedLists(objectAPIName string) []models.RelatedListConfig {
	candidates, err := ms.GetRelatedListCandidates(context.Background(), objectAPIName)
	if err != nil {
		log.Printf("⚠️ seedRelatedLists(%s): candidate query failed, layout will have empty related lists until next restart: %v", objectAPIName, err)
		return []models.RelatedListConfig{}
	}

	var result []models.RelatedListConfig
	for _, c := range candidates {
		if !c.IsRecommended {
			continue
		}
		result = append(result, models.RelatedListConfig{
			ID:             c.ID,
			Label:          c.Label,
			SecondaryLabel: c.SecondaryLabel,
			ObjectAPIName:  c.ObjectAPIName,
			LookupField:    c.LookupField,
			Fields:         append([]string(nil), c.Fields...),
		})
	}
	if result == nil {
		return []models.RelatedListConfig{}
	}

	SortByCuratedOrder(objectAPIName, result)

	return result
}

// repairLayoutRelatedLists fixes stale persisted related list entries at serve time
// (e.g. campaign hierarchy with empty fields, missing labels or secondaryLabel).
// It does NOT discover or append new relationships — layout membership is controlled
// exclusively by bootstrap backfill and admin edits.
func (ms *MetadataService) repairLayoutRelatedLists(ctx context.Context, layout *models.PageLayout) {
	results, err := ms.repo.GetRelatedListConfigs(ctx, layout.ObjectAPIName)
	if err != nil {
		log.Printf("⚠️ repairLayoutRelatedLists(%s): %v", layout.ObjectAPIName, err)
		return
	}

	snap := ms.loadCoreCache()
	discovered := make(map[string]ports.RelatedListConfig, len(results))
	for _, res := range results {
		if isSystemRelatedObject(res.ChildObjectAPI) {
			continue
		}
		discovered[relatedListKey(res.ChildObjectAPI, res.LookupFieldAPI)] = res
	}
	for i := range layout.RelatedLists {
		rl := &layout.RelatedLists[i]
		key := relatedListKey(rl.ObjectAPIName, rl.LookupField)
		cfg, hasCfg := discovered[key]
		label, columns := ms.resolveRelatedListPresentation(snap, layout.ObjectAPIName, rl.ObjectAPIName, rl.LookupField, rl.Label, hasCfg, cfg)
		forceCampaignHierarchy := shouldCurateCampaignHierarchy(layout.ObjectAPIName, rl.ObjectAPIName, rl.LookupField, rl.Label, rl.Fields)

		if (forceCampaignHierarchy || rl.Label == "") && label != "" {
			rl.Label = label
		}
		if (forceCampaignHierarchy || len(rl.Fields) == 0) && len(columns) > 0 {
			rl.Fields = columns
		}
		if resolution := ResolveVirtualRelatedList(rl.ObjectAPIName, rl.LookupField); resolution != nil {
			if replacement := ms.repairVirtualRelatedListFields(*rl, resolution); len(replacement) > 0 {
				rl.Fields = replacement
			}
		}
		if rl.SecondaryLabel == "" {
			// Calculate secondary label from relationship metadata, even if no specific repo config exists
			secondaryLabel := ""
			if hasCfg && cfg.LookupFieldLabel != "" {
				secondaryLabel = cfg.LookupFieldLabel
			} else if hasCfg && cfg.LookupFieldAPI != "" {
				secondaryLabel = cfg.LookupFieldAPI
			} else {
				// Fall back to lookup field from the layout config itself
				secondaryLabel = rl.LookupField
			}
			if secondaryLabel != "" {
				rl.SecondaryLabel = "via " + secondaryLabel
			}
		}
	}
	layout.RelatedLists = ms.ensureRequiredSyntheticRelatedLists(layout.ObjectAPIName, layout.RelatedLists)
}

func (ms *MetadataService) repairVirtualRelatedListFields(
	rl models.RelatedListConfig,
	resolution *RelatedListResolution,
) []string {
	if resolution == nil || resolution.Query.ObjectApiName == "" {
		return nil
	}
	if ms.relatedListFieldsBelongToObject(rl.Fields, resolution.Query.ObjectApiName) {
		return nil
	}
	return ms.defaultRelatedListFields(resolution.Query.ObjectApiName)
}

func (ms *MetadataService) ensureRequiredSyntheticRelatedLists(objectAPIName string, existing []models.RelatedListConfig) []models.RelatedListConfig {
	result := dedupeRelatedLists(existing)
	required := runtimeRuleSet(objectAPIName)
	if len(required) == 0 {
		return result
	}

	seen := make(map[string]bool, len(result))
	for _, rl := range result {
		seen[relatedListDisplayIdentity(rl.ObjectAPIName, rl.LookupField)] = true
	}

	for _, rule := range required {
		key := relatedListDisplayIdentity(rule.ChildObjectAPI, rule.LookupField)
		if seen[key] {
			continue
		}
		candidate := candidateFromRule(rule)
		result = append(result, models.RelatedListConfig{
			ID:             candidate.ID,
			Label:          candidate.Label,
			SecondaryLabel: candidate.SecondaryLabel,
			ObjectAPIName:  candidate.ObjectAPIName,
			LookupField:    candidate.LookupField,
			Fields:         append([]string(nil), candidate.Fields...),
		})
	}

	SortByCuratedOrder(objectAPIName, result)
	return result
}

func dedupeRelatedLists(existing []models.RelatedListConfig) []models.RelatedListConfig {
	if len(existing) < 2 {
		return append([]models.RelatedListConfig(nil), existing...)
	}

	result := make([]models.RelatedListConfig, 0, len(existing))
	seen := make(map[string]bool, len(existing))
	for _, rl := range existing {
		key := relatedListKey(rl.ObjectAPIName, rl.LookupField)
		if seen[key] {
			continue
		}
		seen[key] = true
		result = append(result, rl)
	}
	return result
}

func relatedListKey(childAPI string, lookupField string) string {
	return strings.ToLower(childAPI) + "|" + strings.ToLower(lookupField)
}

func isCampaignHierarchyRelatedList(parentAPI string, childAPI string, lookupField string) bool {
	return strings.EqualFold(parentAPI, "Campaign") &&
		strings.EqualFold(childAPI, "Campaign") &&
		strings.EqualFold(lookupField, "ParentId")
}

func shouldCurateCampaignHierarchy(parentAPI string, childAPI string, lookupField string, label string, fields []string) bool {
	if !isCampaignHierarchyRelatedList(parentAPI, childAPI, lookupField) {
		return false
	}
	if len(fields) == 0 {
		return true
	}
	return label == "" || strings.EqualFold(label, "Campaigns") || strings.EqualFold(label, campaignHierarchyLabel)
}

func (ms *MetadataService) resolveRelatedListPresentation(
	snap *metadataSnapshot,
	parentAPI string,
	childAPI string,
	lookupField string,
	fallbackLabel string,
	hasRepoCfg bool,
	repoCfg ports.RelatedListConfig,
) (string, []string) {
	if isCampaignHierarchyRelatedList(parentAPI, childAPI, lookupField) {
		return campaignHierarchyLabel, append([]string(nil), campaignHierarchyFields...)
	}

	label := fallbackLabel
	if hasRepoCfg && repoCfg.ChildPluralLabel != "" {
		label = repoCfg.ChildPluralLabel
	}
	if label == "" {
		label = childAPI
	}

	fields := ms.defaultRelatedListFields(childAPI)
	if hasRepoCfg {
		if customFields := parseRelatedListFields(repoCfg.RelatedListFields); len(customFields) > 0 {
			fields = customFields
		}
	}

	return label, fields
}

func parseRelatedListFields(raw *string) []string {
	if raw == nil || *raw == "" {
		return nil
	}

	var fields []string
	if err := json.Unmarshal([]byte(*raw), &fields); err != nil || len(fields) == 0 {
		return nil
	}
	return fields
}

func parseConfiguredRelatedListFields(raw *string) []string {
	if raw == nil {
		return nil
	}

	trimmed := strings.TrimSpace(*raw)
	if trimmed == "" {
		return nil
	}

	if strings.HasPrefix(trimmed, "[") {
		var parsed []string
		if err := json.Unmarshal([]byte(trimmed), &parsed); err == nil {
			return normalizeRelatedListFields(parsed)
		}
	}

	parts := strings.FieldsFunc(trimmed, func(r rune) bool {
		return r == ',' || r == '\n' || r == '\r' || r == '\t'
	})
	return normalizeRelatedListFields(parts)
}

func normalizeRelatedListFields(fields []string) []string {
	if len(fields) == 0 {
		return nil
	}

	normalized := make([]string, 0, len(fields))
	seen := make(map[string]bool, len(fields))
	for _, field := range fields {
		trimmed := strings.TrimSpace(strings.Trim(field, `"'`))
		if trimmed == "" {
			continue
		}
		if seen[trimmed] {
			continue
		}
		seen[trimmed] = true
		normalized = append(normalized, trimmed)
	}
	if len(normalized) == 0 {
		return nil
	}
	return normalized
}

func (ms *MetadataService) defaultRelatedListFields(childObjectAPI string) []string {
	nameField := constants.FieldName
	snap := ms.loadCoreCache()
	if snap != nil {
		if s, ok := snap.schemaMap[strings.ToLower(childObjectAPI)]; ok {
			schemaName := make(map[string]string, len(s.Fields))
			for _, f := range s.Fields {
				schemaName[strings.ToLower(f.APIName)] = f.APIName
				if f.IsNameField {
					nameField = f.APIName
				}
			}

			if len(s.ListFields) > 0 {
				filtered := make([]string, 0, len(s.ListFields))
				for _, fieldName := range s.ListFields {
					if canonical, exists := schemaName[strings.ToLower(fieldName)]; exists {
						filtered = append(filtered, canonical)
					}
				}
				if len(filtered) > 0 {
					return filtered
				}
			}

		}
	}
	return []string{nameField, constants.FieldCreatedDate}
}

func (ms *MetadataService) relatedListFieldsBelongToObject(fieldNames []string, objectAPI string) bool {
	if len(fieldNames) == 0 {
		return false
	}
	snap := ms.loadCoreCache()
	if snap == nil {
		return false
	}
	schema, ok := snap.schemaMap[strings.ToLower(objectAPI)]
	if !ok || schema == nil {
		return false
	}
	availableFields := make(map[string]struct{}, len(schema.Fields))
	for _, field := range schema.Fields {
		availableFields[strings.ToLower(field.APIName)] = struct{}{}
	}
	for _, fieldName := range fieldNames {
		if _, exists := availableFields[strings.ToLower(fieldName)]; !exists {
			return false
		}
	}
	return true
}

func (ms *MetadataService) resolveRelatedListFields(childObjectAPI string, configured *string) []string {
	if parsed := parseConfiguredRelatedListFields(configured); len(parsed) > 0 {
		return parsed
	}
	return ms.defaultRelatedListFields(childObjectAPI)
}

// isSystemRelatedObject returns true for system/internal objects that should not
// appear in default page layout related lists (matching Salesforce behavior).
func isSystemRelatedObject(apiName string) bool {
	lower := strings.ToLower(apiName)

	// Exact matches for known non-queryable, system-only, or internal SObjects
	exactMatches := map[string]bool{
		// Non-queryable virtual objects
		"openactivity":           true,
		"activityhistory":        true,
		"processinstancehistory": true,
		// Event/Task relation internals
		"eventwhorelation":       true,
		"eventrelation":          true,
		"acceptedeventrelation":  true,
		"declinedeventrelation":  true,
		"undecidedeventrelation": true,
		"taskwhorelation":        true,
		"taskrelation":           true,
		// Partner/role junction objects
		"accountpartner":         true,
		"opportunitypartner":     true,
		"partner":                true,
		"accountcontactrole":     true,
		"accountcontactrelation": true,
		// Territory system
		"objectterritory2association": true,
		// System config objects
		"userrole": true,
		// Legacy objects
		"attachment": true,
	}
	if exactMatches[lower] {
		return true
	}

	// Suffix matches for system object families
	suffixes := []string{"share", "feed", "history", "changeevent", "fieldhistory"}
	for _, s := range suffixes {
		if strings.HasSuffix(lower, s) {
			return true
		}
	}
	return false
}

// RelatedListCandidate is a structured candidate for the layout editor.
type RelatedListCandidate struct {
	ID             string   `json:"id"`
	ObjectAPIName  string   `json:"objectApiName"`
	LookupField    string   `json:"lookupField"`
	Label          string   `json:"label"`
	SecondaryLabel string   `json:"secondaryLabel"`
	Fields         []string `json:"fields"`
	FieldLabel     string   `json:"fieldLabel"`
	IsSystem       bool     `json:"isSystem"`
	IsRecommended  bool     `json:"isRecommended"`
	Bucket         string   `json:"bucket"`
}

// GetRelatedListCandidates returns structured related-list candidates for a parent object.
// This is the backend contract for the layout editor's candidate picker.
func (ms *MetadataService) GetRelatedListCandidates(ctx context.Context, objectAPIName string) ([]RelatedListCandidate, error) {
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("⚠️ Failed to initialize core cache in GetRelatedListCandidates(%s): %v", objectAPIName, err)
	}

	results, err := ms.repo.GetRelatedListConfigs(ctx, objectAPIName)
	if err != nil {
		return nil, err
	}

	candidates := make([]RelatedListCandidate, 0, len(results))
	for _, res := range results {
		fieldLabel := res.LookupFieldLabel
		if fieldLabel == "" {
			fieldLabel = res.LookupFieldAPI
		}
		// Always use ChildPluralLabel (e.g., "Contacts", "Opportunities").
		// RelatedListLabel from _System_Relationship contains relationship names
		// (e.g., "Account__r") which are not user-friendly display labels.
		label := res.ChildPluralLabel

		candidates = append(candidates, RelatedListCandidate{
			ID:             fmt.Sprintf("%s-%s", res.ChildObjectAPI, res.LookupFieldAPI),
			ObjectAPIName:  res.ChildObjectAPI,
			LookupField:    res.LookupFieldAPI,
			Label:          label,
			SecondaryLabel: "via " + fieldLabel,
			Fields:         ms.resolveRelatedListFields(res.ChildObjectAPI, res.RelatedListFields),
			FieldLabel:     fieldLabel,
		})
	}

	return ResolveRelatedListCandidates(objectAPIName, candidates), nil
}

// GetAllLayouts returns all layouts for an object (no fallback generation)
func (ms *MetadataService) GetAllLayouts(ctx context.Context, objectAPIName string) ([]*models.PageLayout, error) {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	return ms.repo.GetLayouts(ctx, objectAPIName)
}

// GetLayoutByID returns a single layout by ID
func (ms *MetadataService) GetLayoutByID(ctx context.Context, layoutID string) (*models.PageLayout, error) {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	return ms.repo.GetLayout(ctx, layoutID)
}

// SaveLayout saves or updates a page layout
func (ms *MetadataService) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	return ms.repo.SaveLayout(ctx, layout)
}

// MarkLayoutCustomized sets IsCustomized=1 so bootstrap won't overwrite admin changes
func (ms *MetadataService) MarkLayoutCustomized(ctx context.Context, layoutID string) {
	ms.repo.MarkLayoutCustomized(ctx, layoutID)
}

// SetDefaultLayout sets a layout as the default for its object.
// actorID is the UI user who authored the wildcard routing choice.
func (ms *MetadataService) SetDefaultLayout(ctx context.Context, objectAPIName string, layoutID string, actorID *string) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()
	return ms.repo.SetDefaultLayout(ctx, objectAPIName, layoutID, actorID)
}

// DeleteLayout soft-deletes a layout and cleans up any LayoutAssignment rows pointing to it.
// Without this cleanup, FindBestAssignment would return the deleted layout's assignment,
// causing repeated misses and unnecessary auto-generation fallbacks.
func (ms *MetadataService) DeleteLayout(ctx context.Context, layoutID string) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	if err := ms.repo.DeleteAssignmentsByLayoutID(ctx, layoutID); err != nil {
		return fmt.Errorf("failed to clean up assignments for layout %s: %w", layoutID, err)
	}
	return ms.repo.DeleteLayout(ctx, layoutID)
}

// AssignLayoutToProfile assigns a layout to a profile
func (ms *MetadataService) AssignLayoutToProfile(ctx context.Context, profileID, objectAPIName, layoutID string) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	if obj, err := ms.repo.GetSchemaByAPIName(ctx, objectAPIName); err == nil && obj != nil {
		objectAPIName = obj.APIName
	}

	return ms.repo.AssignLayoutToProfile(ctx, profileID, objectAPIName, layoutID)
}

// --- Layout Assignment CRUD (new specificity-based system) ---

// GetAssignmentsForObject returns all assignments for an object.
func (ms *MetadataService) GetAssignmentsForObject(ctx context.Context, objectAPIName string) ([]*models.LayoutAssignment, error) {
	return ms.repo.GetAssignmentsForObject(ctx, objectAPIName)
}

// CreateAssignment creates a new layout assignment.
func (ms *MetadataService) CreateAssignment(ctx context.Context, assignment *models.LayoutAssignment) error {
	assignment.ComputeSpecificity()
	assignment.RouteSource = normalizeAssignmentRouteSource(assignment.RouteSource)
	if assignment.RouteSource == nil {
		return fmt.Errorf("routeSource is required")
	}
	return ms.repo.CreateAssignment(ctx, assignment)
}

// DeleteAssignment removes a layout assignment by ID.
func (ms *MetadataService) DeleteAssignment(ctx context.Context, id string) error {
	return ms.repo.DeleteAssignment(ctx, id)
}

// GetAllApprovalLayouts returns all approval layouts for an object.
func (ms *MetadataService) GetAllApprovalLayouts(ctx context.Context, objectAPIName string) ([]*models.ApprovalPageLayout, error) {
	return ms.repo.GetApprovalLayouts(ctx, objectAPIName)
}

// GetApprovalLayoutByID retrieves a specific approval layout by ID.
func (ms *MetadataService) GetApprovalLayoutByID(ctx context.Context, layoutID string) (*models.ApprovalPageLayout, error) {
	return ms.repo.GetApprovalLayout(ctx, layoutID)
}

// SaveApprovalLayout persists an approval layout.
func (ms *MetadataService) SaveApprovalLayout(ctx context.Context, layout *models.ApprovalPageLayout) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	return ms.repo.SaveApprovalLayout(ctx, layout)
}

// MarkApprovalLayoutCustomized marks an approval layout as admin-modified so bootstrap won't overwrite it.
func (ms *MetadataService) MarkApprovalLayoutCustomized(ctx context.Context, layoutID string) {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	ms.repo.MarkLayoutCustomized(ctx, layoutID) // Reuse existing method name pattern
}

// SetDefaultApprovalLayout marks an approval layout as the default for its object.
func (ms *MetadataService) SetDefaultApprovalLayout(ctx context.Context, objectAPIName string, layoutID string) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	return ms.repo.SetDefaultApprovalLayout(ctx, objectAPIName, layoutID)
}

// DeleteApprovalLayout removes an approval layout by ID.
func (ms *MetadataService) DeleteApprovalLayout(ctx context.Context, layoutID string) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	// Note: We don't delete assignments for approval layouts as they may not use the assignment system
	return ms.repo.DeleteApprovalLayout(ctx, layoutID)
}

// addFieldToLayout adds a new field to ALL layouts for the object (not just the first).
// Follows Salesforce's "fields must never disappear" principle.
// NOTE: Assumes ms.mu is already locked
func (ms *MetadataService) addFieldToLayout(ctx context.Context, objectAPIName, fieldAPIName string) error {
	// Lock held by caller

	layouts, err := ms.repo.GetLayouts(ctx, objectAPIName)
	if err != nil {
		return apperrors.NewInternalError("failed to query layouts", err)
	}
	if len(layouts) == 0 {
		return nil
	}

	for _, layout := range layouts {
		if len(layout.Sections) == 0 {
			continue
		}

		// Check if field already exists in any section
		fieldExists := false
		for _, section := range layout.Sections {
			for _, f := range section.Fields {
				if f == fieldAPIName {
					fieldExists = true
					break
				}
			}
			if fieldExists {
				break
			}
		}

		if !fieldExists {
			// Add to the last non-system section (or first section as fallback)
			targetIdx := 0
			for i := len(layout.Sections) - 1; i >= 0; i-- {
				label := strings.ToLower(layout.Sections[i].Label)
				if label != "system information" && label != "system" {
					targetIdx = i
					break
				}
			}
			layout.Sections[targetIdx].Fields = append(layout.Sections[targetIdx].Fields, fieldAPIName)

			if err := ms.repo.SaveLayout(ctx, layout); err != nil {
				log.Printf("⚠️ Failed to add field %s to layout %s: %v", fieldAPIName, layout.ID, err)
			}
		}
	}

	return nil
}

// ---------------------------------------------------------------------------
// Standard Object Section Templates (Phase 2: Better defaults)
// ---------------------------------------------------------------------------
// Overrides generic section labels with Salesforce-parity names for standard objects.
// Also defines object-specific "extra" sections with field classification rules.

// standardSectionLabels maps standard object names to custom section label overrides.
// Keys are section type constants, values are the Salesforce-parity label.
type sectionLabels struct {
	KeyInfo     string
	Address     string
	Additional  string
	Description string
	System      string
}

var standardSectionLabels = map[string]sectionLabels{
	"Account":     {KeyInfo: "Account Information", Address: "Address Information", Additional: "Additional Information", Description: "Description", System: "System Information"},
	"Contact":     {KeyInfo: "Contact Information", Address: "Address", Additional: "Additional Information", Description: "Description", System: "System Information"},
	"Lead":        {KeyInfo: "Lead Information", Address: "Address", Additional: "Additional Information", Description: "Description", System: "System Information"},
	"Opportunity": {KeyInfo: "Opportunity Information", Additional: "Additional Information", Description: "Description", System: "System Information"},
	"Campaign":    {KeyInfo: "Campaign Information", Description: "Description", System: "System Information"},
	"Case":        {KeyInfo: "Case Information", Additional: "Description Information", System: "System Information"},
	"Quote":       {KeyInfo: "Quote Information", Description: "Description", System: "System Information"},
	"Order":       {KeyInfo: "Order Information", Description: "Description", System: "System Information"},
	"Contract":    {KeyInfo: "Contract Information", Description: "Description", System: "System Information"},
}

// extraSectionRules defines object-specific extra sections with field matching rules.
// Fields matching these rules are pulled OUT of "Additional Details" into named sections.
type extraSectionRule struct {
	Label  string
	Fields []string // exact API name matches
}

var standardExtraSections = map[string][]extraSectionRule{
	"Campaign": {
		{Label: "Planning", Fields: []string{"BudgetedCost", "ActualCost", "ExpectedRevenue", "NumberSent", "NumberOfResponses", "ExpectedResponse"}},
	},
	"Quote": {
		{Label: "Pricing", Fields: []string{"Subtotal", "TotalPrice", "Discount", "Tax", "GrandTotal"}},
		{Label: "Prepared For", Fields: []string{"Phone", "Fax", "Email", "BillingStreet", "BillingCity", "BillingState", "BillingPostalCode", "BillingCountry", "ShippingStreet", "ShippingCity", "ShippingState", "ShippingPostalCode", "ShippingCountry"}},
	},
	"Order": {
		{Label: "Pricing", Fields: []string{"TotalAmount"}},
	},
	"Account": {
		{Label: "Additional Information", Fields: []string{"AccountSource", "AnnualRevenue", "Rating", "NumberOfEmployees"}},
	},
	"Contact": {
		{Label: "Additional Information", Fields: []string{"LeadSource", "AssistantName", "Birthdate", "AssistantPhone"}},
	},
	"Lead": {
		{Label: "Additional Information", Fields: []string{"AnnualRevenue", "NumberOfEmployees", "Rating"}},
	},
	"Opportunity": {
		{Label: "Additional Information", Fields: []string{"LeadSource", "NextStep", "CampaignId"}},
	},
}

// getSectionLabel returns the object-specific or default label for a section type.
func getSectionLabel(objectAPIName, defaultLabel string) string {
	if labels, ok := standardSectionLabels[objectAPIName]; ok {
		switch defaultLabel {
		case constants.SectionLabelInformation:
			if labels.KeyInfo != "" {
				return labels.KeyInfo
			}
		case constants.SectionLabelAddress:
			if labels.Address != "" {
				return labels.Address
			}
		case constants.SectionLabelDescription:
			if labels.Description != "" {
				return labels.Description
			}
		case constants.SectionLabelDetails:
			if labels.Additional != "" {
				return labels.Additional
			}
		case constants.SectionLabelSystemInfo:
			if labels.System != "" {
				return labels.System
			}
		}
	}
	return defaultLabel
}

// extractExtraSections pulls fields matching extra section rules out of detailFields
// and returns them as named sections. Remaining fields are returned as ungrouped.
func extractExtraSections(objectAPIName string, detailFields []models.FieldMetadata) ([]models.PageSection, []models.FieldMetadata) {
	rules, ok := standardExtraSections[objectAPIName]
	if !ok {
		return nil, detailFields
	}

	// Build a set of fields claimed by extra sections
	claimed := make(map[string]string) // fieldAPIName -> section label
	for _, rule := range rules {
		for _, f := range rule.Fields {
			claimed[f] = rule.Label
		}
	}

	// Partition detailFields into extra section buckets and remaining
	buckets := make(map[string][]models.FieldMetadata)
	var remaining []models.FieldMetadata
	for _, f := range detailFields {
		if label, ok := claimed[f.APIName]; ok {
			buckets[label] = append(buckets[label], f)
		} else {
			remaining = append(remaining, f)
		}
	}

	// Build sections in rule order
	var sections []models.PageSection
	for _, rule := range rules {
		if fields, ok := buckets[rule.Label]; ok && len(fields) > 0 {
			sections = append(sections, makeSection(rule.Label, 2, fields))
		}
	}

	return sections, remaining
}

// ---------------------------------------------------------------------------
// Smart Layout Generation
// ---------------------------------------------------------------------------
// Generates a multi-section layout from schema metadata by classifying fields
// into meaningful groups based on field name patterns and types. This replaces
// the naive "dump everything into one section" approach.
//
// Section order:
//   1. Key Information  — Name, Status, Type, OwnerId, required fields, references
//   2. Address Information — Billing*, Shipping*, Mailing*, Other* address fields
//   3. Description      — textarea / richtextarea / longtextarea fields
//   4. Additional Details — everything else
//   5. System Information — audit trail fields (created/modified by/date)
//
// Within each section, required fields come first. The compact layout picks
// the 7 most important fields: Name field first, then required, then references.

// fieldSection classifies which section a field belongs to.
type fieldSection int

const (
	sectionKeyInfo fieldSection = iota
	sectionAddress
	sectionDescription
	sectionDetails
	sectionSystem
)

// addressPrefixes lists the Salesforce address-component prefixes.
// A field whose name starts with one of these (case-insensitive) is an address field.
var addressPrefixes = []string{
	"billing", "shipping", "mailing", "other",
}

// addressSuffixes are the component names that, combined with a prefix, form an address field.
var addressSuffixes = []string{
	"street", "city", "state", "statecode", "country", "countrycode",
	"postalcode", "latitude", "longitude", "geocodeaccuracy",
}

// standaloneAddressNames matches address fields that have no prefix (e.g., on Lead).
var standaloneAddressNames = map[string]bool{
	"street": true, "city": true, "state": true, "statecode": true,
	"country": true, "countrycode": true, "postalcode": true,
	"latitude": true, "longitude": true, "geocodeaccuracy": true,
}

// keyFieldNames are fields that belong in Key Information regardless of type.
var keyFieldNames = map[string]bool{
	"name": true, "firstname": true, "lastname": true,
	"status": true, "stagename": true, "type": true,
	"ownerid": true, "accountid": true, "contactid": true,
	"phone": true, "email": true, "website": true,
	"title": true, "company": true, "industry": true,
	"amount": true, "closedate": true, "probability": true,
	"leadsource": true, "rating": true, "priority": true,
	"recordtypeid": true, "parentid": true,
}

// isLayoutInvisibleField returns true for fields that should NEVER appear in any
// page layout. This is a narrow list — OwnerId, CreatedById, etc. are NOT invisible
// (they appear in Salesforce layouts in "Information" and "System Information" sections).
func isLayoutInvisibleField(apiName string) bool {
	switch apiName {
	case constants.FieldID, constants.FieldIsDeleted, constants.FieldSystemModstamp,
		// Password is never shown in page layouts (Salesforce uses a separate
		// "Reset Password" action); security-internal fields likewise hidden.
		constants.FieldPassword, "FailedLoginAttempts", "LockedUntil", "LastLoginDate":
		return true
	}
	return false
}

// systemFieldNames are audit-trail fields that go in the System section.
var systemFieldNames = map[string]bool{
	"createdbyid":      true,
	"createddate":      true,
	"lastmodifiedbyid": true,
	"lastmodifieddate": true,
	"systemmodstamp":   true,
}

// isTextareaType returns true for field types that should go into the Description section.
func isTextareaType(ft models.FieldType) bool {
	return ft == constants.FieldTypeTextarea || ft == constants.FieldTypeRichtextarea
}

// isAddressField checks whether a field API name matches an address pattern.
func isAddressField(apiName string) bool {
	lower := strings.ToLower(apiName)
	// Check standalone address component names (e.g., Street, City on Lead)
	if standaloneAddressNames[lower] {
		return true
	}
	// Check prefixed address fields (e.g., BillingStreet, ShippingCity)
	for _, prefix := range addressPrefixes {
		if strings.HasPrefix(lower, prefix) {
			suffix := lower[len(prefix):]
			for _, s := range addressSuffixes {
				if suffix == s {
					return true
				}
			}
		}
	}
	return false
}

// classifyField determines which section a field belongs to.
func classifyField(f *models.FieldMetadata) fieldSection {
	lower := strings.ToLower(f.APIName)

	// System audit fields
	if systemFieldNames[lower] {
		return sectionSystem
	}

	// Address fields
	if isAddressField(f.APIName) {
		return sectionAddress
	}

	// Textarea / rich text -> Description section
	if isTextareaType(f.Type) {
		return sectionDescription
	}

	// Key information fields: explicit names, or the name field, or required fields,
	// or reference/masterdetail fields (relationships are high-value)
	if keyFieldNames[lower] || f.IsNameField || f.Required ||
		constants.IsReferenceType(f.Type) || f.IsMasterDetail {
		return sectionKeyInfo
	}

	return sectionDetails
}

// extractFieldGroupPrefix returns the first underscore-delimited segment of a
// custom field name (one ending in "__c"). For standard fields or single-word
// custom fields it returns "" (no grouping). Examples:
//
//	ARR_Amount__c      → "ARR"
//	Partner_Name__c    → "Partner"
//	AWS_Account_ID__c  → "AWS"
//	Tags__c            → ""  (no underscore before __c)
//	Name               → ""  (standard field)
func extractFieldGroupPrefix(apiName string) string {
	if !strings.HasSuffix(apiName, "__c") {
		return ""
	}
	base := strings.TrimSuffix(apiName, "__c")
	idx := strings.IndexByte(base, '_')
	if idx <= 0 {
		return ""
	}
	return base[:idx]
}

// humanizeGroupLabel converts a raw field-name prefix into a section heading.
// Short all-caps acronyms (≤5 chars like ARR, AWS, ACV) are kept as-is;
// longer prefixes get title-cased.
func humanizeGroupLabel(prefix string) string {
	if prefix == "" {
		return ""
	}
	if len(prefix) <= 5 && strings.ToUpper(prefix) == prefix {
		return prefix
	}
	return strings.ToUpper(prefix[:1]) + strings.ToLower(prefix[1:])
}

// generateSmartLayout builds a multi-section layout from object schema metadata.
func generateSmartLayout(apiName string, obj *models.ObjectMetadata) *models.PageLayout {
	// Buckets for each section
	var keyFields, addressFields, descFields, detailFields, sysFields []models.FieldMetadata

	for i := range obj.Fields {
		f := &obj.Fields[i]
		// Skip only truly invisible fields (Id, IsDeleted, SystemModstamp).
		// Do NOT skip OwnerId, CreatedById, CreatedDate, LastModifiedById,
		// LastModifiedDate — these appear in Salesforce page layouts
		// (OwnerId in "Information", audit fields in "System Information").
		if isLayoutInvisibleField(f.APIName) {
			continue
		}

		switch classifyField(f) {
		case sectionKeyInfo:
			keyFields = append(keyFields, *f)
		case sectionAddress:
			addressFields = append(addressFields, *f)
		case sectionDescription:
			descFields = append(descFields, *f)
		case sectionSystem:
			// System-named fields that are NOT flagged IsSystem (rare edge case
			// for custom audit-like fields) still go into a system section.
			sysFields = append(sysFields, *f)
		default:
			detailFields = append(detailFields, *f)
		}
	}

	// Sort each bucket: required fields first, then alphabetical by APIName
	sortFields := func(fields []models.FieldMetadata) {
		sort.SliceStable(fields, func(i, j int) bool {
			if fields[i].Required != fields[j].Required {
				return fields[i].Required // required first
			}
			return fields[i].APIName < fields[j].APIName
		})
	}

	// For key fields, apply a priority order: name field > required > reference > alpha
	sort.SliceStable(keyFields, func(i, j int) bool {
		pi := keyFieldPriority(&keyFields[i])
		pj := keyFieldPriority(&keyFields[j])
		if pi != pj {
			return pi < pj
		}
		return keyFields[i].APIName < keyFields[j].APIName
	})
	sortFields(addressFields)
	sortFields(descFields)
	sortFields(detailFields)

	// Sort system fields in canonical order
	sysOrder := map[string]int{
		strings.ToLower(constants.FieldCreatedByID):      0,
		strings.ToLower(constants.FieldCreatedDate):      1,
		strings.ToLower(constants.FieldLastModifiedByID): 2,
		strings.ToLower(constants.FieldLastModifiedDate): 3,
		strings.ToLower(constants.FieldSystemModstamp):   4,
	}
	sort.SliceStable(sysFields, func(i, j int) bool {
		return sysOrder[strings.ToLower(sysFields[i].APIName)] < sysOrder[strings.ToLower(sysFields[j].APIName)]
	})

	// Sub-group custom fields in detailFields by their naming prefix.
	// E.g., ARR_Amount__c and ARR_Type__c share prefix "ARR" → own section.
	prefixGroups := make(map[string][]models.FieldMetadata)
	var ungroupedDetails []models.FieldMetadata
	for _, f := range detailFields {
		prefix := extractFieldGroupPrefix(f.APIName)
		if prefix != "" {
			prefixGroups[prefix] = append(prefixGroups[prefix], f)
		} else {
			ungroupedDetails = append(ungroupedDetails, f)
		}
	}

	// Collect groups meeting the minimum size; move small groups back to ungrouped.
	type prefixSection struct {
		label  string
		fields []models.FieldMetadata
	}
	var groupedSections []prefixSection
	for prefix, fields := range prefixGroups {
		if len(fields) >= minPrefixGroupSize {
			sortFields(fields)
			groupedSections = append(groupedSections, prefixSection{
				label:  humanizeGroupLabel(prefix),
				fields: fields,
			})
		} else {
			ungroupedDetails = append(ungroupedDetails, fields...)
		}
	}

	// Stable alphabetical order for prefix-based sections
	sort.Slice(groupedSections, func(i, j int) bool {
		return groupedSections[i].label < groupedSections[j].label
	})
	sortFields(ungroupedDetails)

	// Extract object-specific extra sections from detail fields (Phase 2: Better defaults)
	// E.g., Campaign "Planning", Quote "Pricing"/"Prepared For"
	extraSections, ungroupedDetails := extractExtraSections(apiName, ungroupedDetails)

	// Build sections — only include non-empty ones
	// Order: Key Info → Address → Extra sections → Prefix groups → Description → Additional Details → System
	var sections []models.PageSection

	if len(keyFields) > 0 {
		sections = append(sections, makeSection(getSectionLabel(apiName, constants.SectionLabelInformation), 2, keyFields))
	}
	if len(addressFields) > 0 {
		sections = append(sections, makeSection(getSectionLabel(apiName, constants.SectionLabelAddress), 2, addressFields))
	}
	// Object-specific extra sections (e.g., Planning, Pricing, Prepared For)
	sections = append(sections, extraSections...)
	for _, gs := range groupedSections {
		sections = append(sections, makeSection(gs.label, 2, gs.fields))
	}
	if len(descFields) > 0 {
		// Description/textarea fields use 1 column for full-width display
		sections = append(sections, makeSection(getSectionLabel(apiName, constants.SectionLabelDescription), 1, descFields))
	}
	if len(ungroupedDetails) > 0 {
		sections = append(sections, makeSection(getSectionLabel(apiName, constants.SectionLabelDetails), 2, ungroupedDetails))
	}
	if len(sysFields) > 0 {
		sections = append(sections, makeSection(getSectionLabel(apiName, constants.SectionLabelSystemInfo), 2, sysFields))
	}

	// Fallback: if no sections were created (unlikely — object with only Id?), add empty one
	if len(sections) == 0 {
		sections = []models.PageSection{
			{ID: GenerateID(), Label: constants.SectionLabelInformation, Columns: 2, Fields: []string{}},
		}
	}

	// Build compact layout: up to 7 most important fields for the highlights panel
	compactLayout := buildCompactLayout(obj, keyFields)

	return &models.PageLayout{
		ID:            "default_" + apiName,
		ObjectAPIName: apiName,
		LayoutName:    constants.LayoutNameDefault,
		Type:          "Detail",
		IsDefault:     true,
		Tabs:          []string{"Details", "Related", "Activity", "Chatter"},
		Sections:      sections,
		CompactLayout: compactLayout,
	}
}

// makeSection creates a PageSection from a slice of field metadata.
func makeSection(label string, columns int, fields []models.FieldMetadata) models.PageSection {
	names := make([]string, len(fields))
	for i, f := range fields {
		names[i] = f.APIName
	}
	return models.PageSection{
		ID:      GenerateID(),
		Label:   label,
		Columns: columns,
		Fields:  names,
	}
}

// keyFieldPriority returns a sort priority for key information fields.
// Lower number = higher priority.
func keyFieldPriority(f *models.FieldMetadata) int {
	if f.IsNameField {
		return 0 // Name field always first
	}
	lower := strings.ToLower(f.APIName)
	if lower == "firstname" || lower == "lastname" {
		return 1 // Name components right after
	}
	if f.Required {
		return 2 // Required fields next
	}
	if f.IsMasterDetail {
		return 3 // Master-detail relationships
	}
	if constants.IsReferenceType(f.Type) {
		return 4 // Other lookup relationships
	}
	return 5 // Other key fields (Status, Type, etc.)
}

// buildCompactLayout selects up to 7 fields for the highlights panel.
// Priority: name field, then required fields, then reference fields, then key fields.
func buildCompactLayout(obj *models.ObjectMetadata, keyFields []models.FieldMetadata) []string {
	const maxCompactFields = 7
	added := make(map[string]bool)
	var result []string

	addIfNew := func(apiName string) {
		if len(result) >= maxCompactFields {
			return
		}
		if added[apiName] {
			return
		}
		added[apiName] = true
		result = append(result, apiName)
	}

	// 1. Name field first (find actual name field from schema)
	for i := range obj.Fields {
		if obj.Fields[i].IsNameField {
			addIfNew(obj.Fields[i].APIName)
			break
		}
	}
	// Fallback: if no IsNameField, try "Name"
	if len(result) == 0 {
		for i := range obj.Fields {
			if obj.Fields[i].APIName == constants.FieldName {
				addIfNew(constants.FieldName)
				break
			}
		}
	}

	// 2. Required fields from key section
	for _, f := range keyFields {
		if f.Required && !f.IsNameField {
			addIfNew(f.APIName)
		}
	}

	// 3. Reference/relationship fields from key section
	for _, f := range keyFields {
		if constants.IsReferenceType(f.Type) || f.IsMasterDetail {
			addIfNew(f.APIName)
		}
	}

	// 4. Remaining key fields (Status, Type, etc.)
	for _, f := range keyFields {
		addIfNew(f.APIName)
	}

	return result
}
