package metadata

import (
	"context"
	"log"
	"strings"
	"sync"
	"sync/atomic"
	"time"

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

// metadataSnapshot holds an immutable snapshot of all cached metadata.
// This is the Copy-on-Write (COW) unit: a new snapshot is built without
// holding any lock, then atomically swapped via MetadataService.cache.
type metadataSnapshot struct {
	// Schemas
	schemas   []*models.ObjectMetadata
	schemaMap map[string]*models.ObjectMetadata
	fieldMap  map[string][]models.FieldMetadata // key: ObjectAPIName (lowercase)
	// Relationships
	relationshipsMap      map[string][]*models.Relationship
	childRelationshipsMap map[string][]*models.ObjectMetadata

	// Flows (Salesforce-compatible FlowDefinition)
	flowDefinitions   []*models.FlowDefinition
	flowDefinitionMap map[string]*models.FlowDefinition // key: flow ID

	// Per-Object Metadata
	validationRulesMap map[string][]*models.ValidationRule // key: objectAPIName (lowercase)
	autoNumbersMap     map[string][]*models.AutoNumber     // key: objectAPIName (lowercase)
	identifierRulesMap map[string]*models.IdentifierRule   // key: objectAPIName (lowercase) - active rule per object
}

// AutoNumberInvalidator is the minimal interface MetadataService uses
// to poke the autonumber MetadataSource when an autonumber field is
// created or batch-created. Declared here (where it is used) per Go
// idiom; implemented by
// `features/automation/autonumber.MetadataSource.InvalidateObject`.
//
// Round-5 Codex V1-P2 / V2-H3: without this wire, the engine's
// snapshot stayed stale until process restart after admin field
// creation, so new autonumber fields silently produced no values on
// INSERT.
type AutoNumberInvalidator interface {
	InvalidateObject(ctx context.Context, objectAPIName string) error
}

// SchemaDecorator augments a loaded object schema with synthesized
// virtual fields at cache-build time. Contract:
//
//   - MUST NOT mutate the input. The input belongs to the cache and is
//     shared with concurrent readers once published.
//   - When adding fields, MUST return a new ObjectMetadata pointer
//     (typically via models.CloneWithAdditionalFields), not a modified
//     version of the input.
//   - For objects the decorator doesn't care about, MAY return the
//     input pointer unchanged — buildCoreSnapshot treats pointer
//     equality as "no decoration applied" and continues with the next
//     decorator. Returning the input is the common-case fast path and
//     is cheaper than a superfluous clone.
//
// Decorators are called from buildCoreSnapshot for every schema, in the
// order they were registered, with the previous decorator's output
// (potentially unchanged) as the next decorator's input.
//
// Returning an error aborts the cache build. Decorators that cannot
// resolve their data source should prefer returning the input unchanged
// (logging internally) over failing a refresh of unrelated metadata.
type SchemaDecorator interface {
	DecorateSchema(ctx context.Context, schema *models.ObjectMetadata) (*models.ObjectMetadata, error)
}

// MetadataService manages CRM metadata
type MetadataService struct {
	schemaMgr *SchemaManager
	repo      ports.MetadataRepository
	txManager ports.TransactionManager

	// cache holds the current full metadata snapshot. Readers load the pointer
	// atomically (lock-free). Writers build a new snapshot in the background,
	// then Store() it atomically - no read blocking during refresh.
	cache atomic.Pointer[metadataSnapshot]
	// coreCache holds the schema/field/relationship graph used by authorization
	// and record semantics. It is populated independently so unrelated failures
	// (for example flow cache rebuilds) cannot erase core metadata.
	coreCache atomic.Pointer[metadataSnapshot]

	// initMu serializes cache initialization (first load) and explicit
	// RefreshCache calls so only one goroutine rebuilds at a time.
	initMu sync.Mutex

	// mu protects repository write operations (CRUD) that are not part of
	// the read cache. It is NOT held during cache refresh reads.
	mu sync.RWMutex

	// Dependencies
	validationSvc         *ValidationService
	eventBus              ports.EventPublisher
	autoNumberInvalidator AutoNumberInvalidator

	// schemaDecorators augment loaded schemas with synthesized virtual
	// fields (e.g. PermissionSet's Permissions<X> boolean surface). They
	// are applied inside buildCoreSnapshot so the cached pointer is
	// already effective — downstream consumers never see the pre-decoration
	// schema and never need to decorate on read.
	//
	// decoratorMu guards schemaDecorators against concurrent RegisterSchemaDecorator
	// calls (rare: registration happens at wire-up) and against concurrent
	// buildCoreSnapshot reads.
	decoratorMu      sync.RWMutex
	schemaDecorators []SchemaDecorator
}

// NewMetadataService creates a new MetadataService
func NewMetadataService(repo ports.MetadataRepository, schemaMgr *SchemaManager, eventBus ports.EventPublisher) *MetadataService {
	return &MetadataService{
		schemaMgr: schemaMgr,
		repo:      repo,
		eventBus:  eventBus,
	}
}

// SetValidationService sets the validation service dependency
func (ms *MetadataService) SetValidationService(vs *ValidationService) {
	ms.validationSvc = vs
}

// SetTransactionManager wires the transaction manager used by flow tooling
// writes so cache invalidation can happen only after the outermost commit.
func (ms *MetadataService) SetTransactionManager(txm ports.TransactionManager) {
	ms.txManager = txm
}

// SetAutoNumberInvalidator wires the autonumber metadata invalidator so
// CreateField / BatchCreateFields can refresh the engine's snapshot
// after writing `_System_Field.auto_number_*`. Nil is valid and
// disables invalidation — required for tests that don't exercise the
// autonumber path.
func (ms *MetadataService) SetAutoNumberInvalidator(inv AutoNumberInvalidator) {
	ms.autoNumberInvalidator = inv
}

// RegisterSchemaDecorator adds a decorator that will be applied to every
// schema during the next cache build. Registration invalidates the
// existing cache so the next read sees the decorated schema without
// requiring an explicit RefreshCache call from the caller.
//
// Nil decorators are silently ignored. Registration is append-order;
// later decorators see the output of earlier ones.
func (ms *MetadataService) RegisterSchemaDecorator(d SchemaDecorator) {
	if d == nil {
		return
	}
	ms.decoratorMu.Lock()
	ms.schemaDecorators = append(ms.schemaDecorators, d)
	ms.decoratorMu.Unlock()
	// Invalidate so the next read rebuilds the snapshot through the new
	// decorator set. Cheap — atomic store of nil, next GetSchema rebuilds.
	ms.InvalidateCache()
}

// snapshotDecorators returns a copy of the current decorator list so
// buildCoreSnapshot can iterate without holding the lock during slow
// catalog reads inside decorator implementations.
func (ms *MetadataService) snapshotDecorators() []SchemaDecorator {
	ms.decoratorMu.RLock()
	defer ms.decoratorMu.RUnlock()
	if len(ms.schemaDecorators) == 0 {
		return nil
	}
	out := make([]SchemaDecorator, len(ms.schemaDecorators))
	copy(out, ms.schemaDecorators)
	return out
}

// RefreshCache reloads all metadata from the database.
// The new cache is built without holding any read-blocking lock, then
// atomically swapped in (COW pattern). Concurrent readers are never blocked.
func (ms *MetadataService) RefreshCache(ctx context.Context) error {
	ms.initMu.Lock()
	defer ms.initMu.Unlock()
	return ms.buildAndSwapCache(ctx)
}

func buildSchemaIndexes(schemas []*models.ObjectMetadata) (map[string]*models.ObjectMetadata, map[string][]models.FieldMetadata) {
	schemaMap := make(map[string]*models.ObjectMetadata)
	fieldMap := make(map[string][]models.FieldMetadata)

	for _, schema := range schemas {
		key := strings.ToLower(schema.APIName)
		schemaMap[key] = schema
		fieldMap[key] = schema.Fields
	}

	return schemaMap, fieldMap
}

func (ms *MetadataService) buildCoreSnapshot(ctx context.Context) (*metadataSnapshot, error) {
	schemas, err := ms.repo.GetAllSchemas(ctx)
	if err != nil {
		return nil, apperrors.NewInternalError("failed to load schemas", err)
	}

	// Apply schema decorators before indexing so downstream consumers see
	// an effective schema (physical fields + synthesized virtual fields).
	// Decorators must not mutate the input; they return a new pointer.
	// Index maps are built from the decorated slice so FieldsMap lookups
	// naturally resolve virtual fields case-insensitively.
	if decorators := ms.snapshotDecorators(); len(decorators) > 0 {
		decorated := make([]*models.ObjectMetadata, len(schemas))
		for i, schema := range schemas {
			current := schema
			for _, d := range decorators {
				next, derr := d.DecorateSchema(ctx, current)
				if derr != nil {
					return nil, apperrors.NewInternalError("schema decorator failed for "+current.APIName, derr)
				}
				if next != nil {
					current = next
				}
			}
			decorated[i] = current
		}
		schemas = decorated
	}

	schemaMap, fieldMap := buildSchemaIndexes(schemas)
	explicitRelationships, err := ms.repo.GetAllRelationships(ctx)
	if err != nil {
		return nil, apperrors.NewInternalError("failed to load relationships", err)
	}
	relationshipsMap, childRelationshipsMap := ms.buildRelationshipSnapshot(schemas, schemaMap, explicitRelationships)

	return &metadataSnapshot{
		schemas:               schemas,
		schemaMap:             schemaMap,
		fieldMap:              fieldMap,
		relationshipsMap:      relationshipsMap,
		childRelationshipsMap: childRelationshipsMap,
	}, nil
}

// buildAndSwapCache builds a new metadataSnapshot from the database (no lock
// held) and atomically publishes it. Caller must hold ms.initMu.
func (ms *MetadataService) buildAndSwapCache(ctx context.Context) error {
	start := time.Now()
	log.Println("\U0001f504 Refreshing metadata cache...")

	// 1. Load the core schema graph first. Authorization depends on this data,
	// so publish it independently before ancillary metadata refresh proceeds.
	log.Println("  [1/5] Loading schema graph...")
	coreSnap, err := ms.buildCoreSnapshot(ctx)
	if err != nil {
		return err
	}
	ms.coreCache.Store(coreSnap)
	log.Printf("  [1/5] Loaded %d schemas in %v", len(coreSnap.schemas), time.Since(start))

	// Virtual schemas have been removed. System objects like Dashboard and Report
	// are now dynamically registered and loaded from the physical _System_Object registry.

	// 2. Load all flow definitions (Salesforce-compatible)
	// Critical: If this fails, returning empty flows would silently disable all automation.
	// We must fail the refresh instead.
	stepStart := time.Now()
	log.Println("  [2/5] Loading flow definitions...")
	flowDefs, err := ms.repo.GetAllFlowDefinitions(ctx)
	if err != nil {
		return apperrors.NewInternalError("failed to load flow definitions", err)
	}
	log.Printf("  [2/5] Loaded %d flows in %v", len(flowDefs), time.Since(stepStart))

	flowDefMap := make(map[string]*models.FlowDefinition)
	for _, flowDef := range flowDefs {
		flowDefMap[flowDef.Id] = flowDef
	}

	// 3. Load ALL validation rules and auto-numbers in batch (2 queries instead of 2*N)
	validationRulesMap := make(map[string][]*models.ValidationRule)
	autoNumbersMap := make(map[string][]*models.AutoNumber)

	stepStart = time.Now()
	log.Println("  [3/5] Loading validation rules...")
	allRules, err := ms.repo.GetAllValidationRules(ctx)
	if err != nil {
		log.Printf("\u26a0\ufe0f Failed to load validation rules: %v", err)
	} else {
		for _, rule := range allRules {
			key := strings.ToLower(rule.ObjectAPIName)
			validationRulesMap[key] = append(validationRulesMap[key], rule)
		}
	}
	log.Printf("  [3/5] Loaded %d validation rules in %v", len(allRules), time.Since(stepStart))

	stepStart = time.Now()
	log.Println("  [4/5] Loading auto-numbers...")
	allAutoNums, err := ms.repo.GetAllAutoNumbers(ctx)
	if err != nil {
		log.Printf("⚠️ Failed to load auto-numbers: %v", err)
	} else {
		for _, an := range allAutoNums {
			key := strings.ToLower(an.ObjectAPIName)
			autoNumbersMap[key] = append(autoNumbersMap[key], an)
		}
	}
	log.Printf("  [4/5] Loaded %d auto-numbers in %v", len(allAutoNums), time.Since(stepStart))

	// 5. Identifier rules (Unified Identifier Engine — see docs/design/unified-identifier-engine.md).
	// Repo returns IsActive=1 rows only, sorted by (ObjectApiName, RuleName) ASC at the
	// SQL layer. The DB index is UNIQUE(ObjectApiName, RuleName) — not unique by IsActive —
	// so multiple active rules per object are technically possible. Until the engine
	// supports rule-set semantics, we keep the lexicographically smallest RuleName per
	// object (the first row returned given the deterministic ORDER BY) and log if a
	// duplicate is seen so operators can detect drift and know which rule won.
	identifierRulesMap := make(map[string]*models.IdentifierRule)
	stepStart = time.Now()
	log.Println("  [5/5] Loading identifier rules...")
	allIdentifierRules, err := ms.repo.GetAllIdentifierRules(ctx)
	if err != nil {
		log.Printf("⚠️ Failed to load identifier rules: %v", err)
	} else {
		for _, rule := range allIdentifierRules {
			key := strings.ToLower(rule.ObjectAPIName)
			if existing, ok := identifierRulesMap[key]; ok {
				// Deterministic tie-break: SQL ORDER BY RuleName ASC means
				// `existing.RuleName` <= `rule.RuleName`, so the kept rule is
				// always the lexicographically smallest active RuleName for
				// this object. Log both so operators can deactivate the loser.
				log.Printf("⚠️ Multiple active identifier rules for %s; keeping %s (RuleName=%q, lex-smallest), ignoring %s (RuleName=%q)",
					rule.ObjectAPIName, existing.ID, existing.RuleName, rule.ID, rule.RuleName)
				continue
			}
			identifierRulesMap[key] = rule
		}
	}
	log.Printf("  [5/5] Loaded %d identifier rules in %v", len(allIdentifierRules), time.Since(stepStart))

	// 6. ATOMIC SWAP - publish the new immutable snapshot.
	// Readers that already loaded the old pointer continue using it safely;
	// new readers will see the fresh data immediately.
	snap := &metadataSnapshot{
		schemas:               coreSnap.schemas,
		schemaMap:             coreSnap.schemaMap,
		fieldMap:              coreSnap.fieldMap,
		relationshipsMap:      coreSnap.relationshipsMap,
		childRelationshipsMap: coreSnap.childRelationshipsMap,
		flowDefinitions:       flowDefs,
		flowDefinitionMap:     flowDefMap,
		validationRulesMap:    validationRulesMap,
		autoNumbersMap:        autoNumbersMap,
		identifierRulesMap:    identifierRulesMap,
	}
	ms.cache.Store(snap)
	ms.coreCache.Store(snap)

	log.Printf("\u2705 Metadata cache refreshed: %d objects, %d flows, %d rules, %d auto-nums, %d identifier rules (total: %v)",
		len(coreSnap.schemas), len(flowDefs), len(allRules), len(allAutoNums), len(allIdentifierRules), time.Since(start))
	return nil
}

// isSystemTableForCaching checks if a table is a system table (for caching optimization)
func isSystemTableForCaching(apiName string) bool {
	return strings.HasPrefix(apiName, "_System_")
}

// loadCache returns the current cache snapshot (lock-free).
// Returns nil if the cache has not been initialized yet.
func (ms *MetadataService) loadCache() *metadataSnapshot {
	return ms.cache.Load()
}

func (ms *MetadataService) loadCoreCache() *metadataSnapshot {
	if snap := ms.coreCache.Load(); snap != nil {
		return snap
	}
	return ms.cache.Load()
}

// ensureCacheInitialized ensures that metadata is loaded (Double-Checked Locking).
// The fast path is a single atomic pointer load (no lock).
func (ms *MetadataService) ensureCacheInitialized(ctx context.Context) error {
	// 1. Fast path: atomic load, no lock
	if ms.cache.Load() != nil {
		return nil
	}

	// 2. Slow path: serialize initialization
	ms.initMu.Lock()
	defer ms.initMu.Unlock()

	// Double check after acquiring lock
	if ms.cache.Load() != nil {
		return nil
	}

	return ms.buildAndSwapCache(ctx)
}

func (ms *MetadataService) ensureCoreCacheInitialized(ctx context.Context) error {
	if ms.loadCoreCache() != nil {
		return nil
	}

	ms.initMu.Lock()
	defer ms.initMu.Unlock()

	if ms.loadCoreCache() != nil {
		return nil
	}

	coreSnap, err := ms.buildCoreSnapshot(ctx)
	if err != nil {
		return err
	}
	ms.coreCache.Store(coreSnap)
	return nil
}

// Getter methods

func (ms *MetadataService) GetSchema(ctx context.Context, apiName string) *models.ObjectMetadata {
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("\u26a0\ufe0f Failed to initialize core cache in GetSchema: %v", err)
		return nil
	}

	snap := ms.loadCoreCache()
	if snap == nil {
		if ms.repo == nil {
			return nil
		}
		schema, err := ms.repo.GetSchemaByAPIName(ctx, apiName)
		if err != nil {
			log.Printf("\u26a0\ufe0f Failed to load schema %s from repository after empty core cache: %v", apiName, err)
			return nil
		}
		return schema
	}

	normalizedName := strings.ToLower(apiName)
	if schema, ok := snap.schemaMap[normalizedName]; ok {
		return schema
	}
	if ms.repo == nil {
		return nil
	}

	schema, err := ms.repo.GetSchemaByAPIName(ctx, apiName)
	if err != nil {
		log.Printf("\u26a0\ufe0f Failed to load schema %s from repository after cache miss: %v", apiName, err)
		return nil
	}
	return schema
}

// InvalidateCache clears the cache, forcing a refresh on next read (Thread-safe).
// Uses atomic store - no lock needed, readers never block.
func (ms *MetadataService) InvalidateCache() {
	ms.cache.Store(nil)
	ms.coreCache.Store(nil)
	log.Println("\U0001f5d1\ufe0f Metadata cache invalidated")
}

// invalidateCacheLocked clears the cache. With the COW pattern the underlying
// operation is an atomic pointer store, so holding a lock is harmless but no
// longer required. Kept for callers that already hold ms.mu.
func (ms *MetadataService) invalidateCacheLocked() {
	ms.cache.Store(nil)
	ms.coreCache.Store(nil)
}

// GetSchemaOrError returns the schema or a NotFoundError if not found
func (ms *MetadataService) GetSchemaOrError(ctx context.Context, apiName string) (*models.ObjectMetadata, error) {
	schema := ms.GetSchema(ctx, apiName)
	if schema == nil {
		return nil, apperrors.NewNotFoundError("Object Metadata", apiName)
	}
	return schema, nil
}

func (ms *MetadataService) GetField(objectAPIName, fieldAPIName string) *models.FieldMetadata {
	// Ensure cache is loaded
	// Use Background() with Timeout to avoid hang since signature lacks Context.
	// Given strict audit, we use a timeout context.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("\u26a0\ufe0f Failed to initialize core cache in GetField: %v", err)
		return nil
	}

	snap := ms.loadCoreCache()
	if snap == nil {
		return nil
	}

	normalizedObj := strings.ToLower(objectAPIName)
	if fields, ok := snap.fieldMap[normalizedObj]; ok {
		for _, field := range fields {
			if field.APIName == fieldAPIName {
				result := field // Copy to be safe
				return &result
			}
		}
	}

	return nil
}

// ConvertedStateMetadata describes the Salesforce converted-record shape for
// an object. MetadataService owns this shape fact so runtime consumers do not
// duplicate Converted* lookup discovery.
type ConvertedStateMetadata struct {
	StateField   models.FieldMetadata
	DateField    *models.FieldMetadata
	ActorField   *models.FieldMetadata
	LookupFields []models.FieldMetadata
}

// ConvertedLookups returns the lookup fields on an object whose APIName
// matches the Salesforce converted-anchor conventions: standard lookups use
// "Converted<Target>Id" (e.g. Lead's ConvertedAccountId /
// ConvertedContactId / ConvertedOpportunityId), while custom lookups use the
// normal Salesforce custom-field suffix ("Converted<Target>__c").
// Results preserve the order declared in ObjectMetadata.Fields.
//
// Used by the generic standard-action dispatcher (Lead Convert and any
// future convert-style action) to pre-resolve the anchor set that seeded
// flows iterate over, so a tenant can add a custom Converted* lookup and
// have it picked up without a code change.
func (ms *MetadataService) ConvertedLookups(ctx context.Context, objectAPIName string) []models.FieldMetadata {
	schema := ms.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return nil
	}
	return convertedLookupsFromFields(schema.Fields)
}

// ConvertedStateMetadata returns the metadata descriptor for converted-record
// state when an object declares the standard boolean state field and at least
// one converted-anchor lookup.
func (ms *MetadataService) ConvertedStateMetadata(ctx context.Context, objectAPIName string) *ConvertedStateMetadata {
	schema := ms.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return nil
	}

	var stateField *models.FieldMetadata
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if field.APIName != "IsConverted" {
			continue
		}
		if constants.NormalizeFieldType(string(field.Type)) != constants.FieldTypeBoolean {
			continue
		}
		copy := *field
		stateField = &copy
		break
	}
	if stateField == nil {
		return nil
	}

	lookups := convertedLookupsFromFields(schema.Fields)
	if len(lookups) == 0 {
		return nil
	}

	return &ConvertedStateMetadata{
		StateField:   *stateField,
		DateField:    convertedDateField(schema.Fields),
		ActorField:   convertedActorField(schema.Fields),
		LookupFields: lookups,
	}
}

func convertedDateField(fields []models.FieldMetadata) *models.FieldMetadata {
	for i := range fields {
		field := &fields[i]
		if field.APIName != "ConvertedDate" {
			continue
		}
		t := constants.NormalizeFieldType(string(field.Type))
		if t != constants.FieldTypeDate && t != constants.FieldTypeDatetime {
			continue
		}
		copy := *field
		return &copy
	}
	return nil
}

func convertedActorField(fields []models.FieldMetadata) *models.FieldMetadata {
	for i := range fields {
		field := &fields[i]
		if field.APIName != constants.FieldLastModifiedByID {
			continue
		}
		if constants.NormalizeFieldType(string(field.Type)) != constants.FieldTypeReference {
			continue
		}
		copy := *field
		return &copy
	}
	return nil
}

func convertedLookupsFromFields(fields []models.FieldMetadata) []models.FieldMetadata {
	out := make([]models.FieldMetadata, 0)
	for _, field := range fields {
		if !isConvertedLookupAPIName(field.APIName) {
			continue
		}
		if len(field.ReferenceTo) == 0 {
			continue
		}
		out = append(out, field)
	}
	return out
}

func isConvertedLookupAPIName(name string) bool {
	const prefix = "Converted"
	if !strings.HasPrefix(name, prefix) {
		return false
	}
	for _, suffix := range []string{"Id", "__c"} {
		if len(name) > len(prefix)+len(suffix) && strings.HasSuffix(name, suffix) {
			return true
		}
	}
	return false
}

func (ms *MetadataService) GetSchemas(ctx context.Context) []*models.ObjectMetadata {
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("\u26a0\ufe0f Failed to initialize core cache in GetSchemas: %v", err)
		return []*models.ObjectMetadata{}
	}

	snap := ms.loadCoreCache()
	if snap != nil {
		return snap.schemas
	}

	return []*models.ObjectMetadata{}
}

// GetSearchableObjects returns all objects marked as searchable in metadata.
// This replaces hardcoded CRM object lists and provides dynamic search scope.
func (ms *MetadataService) GetSearchableObjects(ctx context.Context) []string {
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("\u26a0\ufe0f Failed to initialize core cache in GetSearchableObjects: %v", err)
		return []string{}
	}

	snap := ms.loadCoreCache()
	if snap == nil {
		return []string{}
	}

	var searchable []string
	for _, schema := range snap.schemas {
		if IsUserSearchableObject(schema) {
			searchable = append(searchable, schema.APIName)
		}
	}
	return searchable
}

func IsUserSearchableObject(schema *models.ObjectMetadata) bool {
	if schema == nil || schema.IsSystem || !schema.Searchable {
		return false
	}
	if constants.IsManagedSystemTable(schema.APIName) {
		return false
	}

	switch schema.TableType {
	case constants.TableTypeStandardObject:
		return true
	case constants.TableTypeCustomObject:
		return schema.IsCustom
	case "":
		return true
	default:
		return false
	}
}

// publishMetadataEvent ensures metadata changes are broadcast to the system
func (ms *MetadataService) publishMetadataEvent(ctx context.Context, eventType events.EventType, payload interface{}) {
	if ms.eventBus != nil {
		if err := ms.eventBus.Publish(ctx, eventType, payload); err != nil {
			log.Printf("\u26a0\ufe0f Failed to publish metadata event %s: %v", eventType, err)
		}
	}
}
