package metadata

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

	"github.com/nexuscrm/events"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	domainrls "github.com/nexuscrm/kernel/pkg/rls"
	domainSchema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/searchmetadata"
)

// 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)
	objectByKeyPrefixMap map[string]string                 // key: KeyPrefix (case-sensitive)
	keyPrefixByObjectMap map[string]string                 // 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)
}

// 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
}

// FormulaViewRebuilder is the narrow runtime hook MetadataService uses to
// fail formula field edits closed after the metadata row is updated.
type FormulaViewRebuilder interface {
	RebuildView(ctx context.Context, objectAPIName string) error
}

// ValidationProjectionInvalidator invalidates the precomputed validation
// projection closure when validation rules change on an object.
type ValidationProjectionInvalidator interface {
	InvalidateValidationProjection(objectName string)
}

// 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
	// runtimeView resolves the immutable request-bound view installed by
	// Runtime ServingAdmission. When set, core metadata getters never consult
	// process-global caches or repository fallback.
	runtimeView RuntimeMetadataViewResolver

	// 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
	autoNumberFieldResolve AutoNumberFieldResolver
	formulaViewRebuilder   FormulaViewRebuilder
	projectionInvalidator  ValidationProjectionInvalidator
	listViewRLSProvider    listViewRLSProvider

	cacheInvalidationHookMu sync.RWMutex
	cacheInvalidationHooks  []func()

	triggerSubscriptionRuntime triggerSubscriptionRuntimeCache
	triggerSubscriptionList    triggerSubscriptionListLoader
	triggerSubscriptionHasAny  triggerSubscriptionHasAnyLoader

	// 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
}

// RuntimeMetadataViewResolver is the narrow Runtime-only composition seam. It
// avoids adding serving state to public contracts or broad engine interfaces.
type RuntimeMetadataViewResolver func(context.Context) (*catalogsnapshot.RuntimeMetadataView, bool)

type listViewRLSProvider interface {
	BuildRLSResult(ctx context.Context, schema *models.ObjectMetadata, objectName string, user *models.UserSession) *domainrls.Result
}

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

// NewRuntimeMetadataService creates the fail-closed metadata reader used by an
// independently serving Runtime. It has no repository and no process-global
// current view; every read must resolve the exact view from request context.
func NewRuntimeMetadataService(resolver RuntimeMetadataViewResolver) (*MetadataService, error) {
	if resolver == nil {
		return nil, errors.New("runtime metadata service: request-bound view resolver is required")
	}
	return &MetadataService{runtimeView: resolver}, nil
}

// UsesRequestBoundRuntimeView lets composition and permission caching avoid
// global preparation/caches for immutable request-scoped metadata.
func (ms *MetadataService) UsesRequestBoundRuntimeView() bool {
	return ms != nil && ms.runtimeView != nil
}

// BoundRuntimeMetadataView returns the exact immutable view selected for this
// execution. It is used by durable-work producers to persist the content
// binding that a separate Runtime Worker must later reconstruct. Legacy
// metadata services deliberately return false; they do not invent a revision.
func (ms *MetadataService) BoundRuntimeMetadataView(ctx context.Context) (*catalogsnapshot.RuntimeMetadataView, bool) {
	if ms == nil || ms.runtimeView == nil {
		return nil, false
	}
	return ms.runtimeView(ctx)
}

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

// SetFormulaViewRebuilder wires synchronous active-formula view rebuilds.
func (ms *MetadataService) SetFormulaViewRebuilder(rebuilder FormulaViewRebuilder) {
	ms.formulaViewRebuilder = rebuilder
}

// SetValidationProjectionInvalidator wires the invalidator called when
// validation rules change so the projection closure is recomputed on next
// view rebuild.
func (ms *MetadataService) SetValidationProjectionInvalidator(inv ValidationProjectionInvalidator) {
	ms.projectionInvalidator = inv
}

// SetListViewRLSProvider wires the generic record-read policy compiler used by
// caller-scoped list-view catalog reads.
func (ms *MetadataService) SetListViewRLSProvider(provider listViewRLSProvider) {
	ms.listViewRLSProvider = provider
}

// 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
}

// SetAutoNumberFieldResolver wires the autonumber field resolver so layout
// generation can exclude autonumber fields from editable sections. Nil is
// valid — falls back to v1 type-only detection.
func (ms *MetadataService) SetAutoNumberFieldResolver(r AutoNumberFieldResolver) {
	ms.autoNumberFieldResolve = r
}

// RegisterCacheInvalidationHook wires process-local caches whose content is
// derived from metadata tables but is not owned by MetadataService's snapshot.
// Hooks run after RefreshCache succeeds and after explicit cache invalidation.
func (ms *MetadataService) RegisterCacheInvalidationHook(hook func()) {
	if hook == nil {
		return
	}
	ms.cacheInvalidationHookMu.Lock()
	ms.cacheInvalidationHooks = append(ms.cacheInvalidationHooks, hook)
	ms.cacheInvalidationHookMu.Unlock()
}

func (ms *MetadataService) notifyCacheInvalidationHooks() {
	ms.cacheInvalidationHookMu.RLock()
	hooks := append([]func(){}, ms.cacheInvalidationHooks...)
	ms.cacheInvalidationHookMu.RUnlock()

	for _, hook := range hooks {
		hook()
	}
}

// autoNumberFieldsForObject returns the set of autonumber field API names for
// the given object, or nil when the resolver is unwired.
func (ms *MetadataService) autoNumberFieldsForObject(objectAPIName string) map[string]struct{} {
	if ms.autoNumberFieldResolve == nil {
		return nil
	}
	return ms.autoNumberFieldResolve(objectAPIName)
}

// 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 {
	if ms.UsesRequestBoundRuntimeView() {
		return errors.New("runtime metadata service: process-global refresh is disabled")
	}
	ms.initMu.Lock()
	defer ms.initMu.Unlock()
	// Invalidate derived caches BEFORE publishing fresh metadata so concurrent
	// readers cannot observe new rules with a stale projection closure.
	ms.clearTriggerSubscriptionRuntimeCache()
	ms.notifyCacheInvalidationHooks()
	if err := ms.buildAndSwapCache(ctx); err != nil {
		return err
	}
	return nil
}

func buildSchemaIndexes(schemas []*models.ObjectMetadata) (map[string]*models.ObjectMetadata, map[string][]models.FieldMetadata, map[string]string, map[string]string, error) {
	schemaMap := make(map[string]*models.ObjectMetadata)
	fieldMap := make(map[string][]models.FieldMetadata)
	objectByKeyPrefixMap, keyPrefixByObjectMap, err := buildObjectIdentityIndexes(schemas)
	if err != nil {
		return nil, nil, nil, nil, err
	}

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

	return schemaMap, fieldMap, objectByKeyPrefixMap, keyPrefixByObjectMap, nil
}

func buildObjectIdentityIndexes(schemas []*models.ObjectMetadata) (map[string]string, map[string]string, error) {
	objectByKeyPrefixMap := make(map[string]string)
	keyPrefixByObjectMap := make(map[string]string)

	for _, schema := range schemas {
		if schema == nil {
			continue
		}
		objectAPIName := strings.TrimSpace(schema.APIName)
		if objectAPIName == "" || schema.KeyPrefix == nil {
			continue
		}
		keyPrefix := strings.TrimSpace(*schema.KeyPrefix)
		if keyPrefix == "" {
			continue
		}
		objectKey := strings.ToLower(objectAPIName)
		keyPrefixByObjectMap[objectKey] = keyPrefix
		if currentObjectAPIName, ok := objectByKeyPrefixMap[keyPrefix]; ok && !strings.EqualFold(currentObjectAPIName, objectAPIName) {
			return nil, nil, fmt.Errorf("duplicate key prefix %s maps to both %s and %s", keyPrefix, currentObjectAPIName, objectAPIName)
		}
		objectByKeyPrefixMap[keyPrefix] = objectAPIName
	}

	return objectByKeyPrefixMap, keyPrefixByObjectMap, nil
}

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, objectByKeyPrefixMap, keyPrefixByObjectMap, err := buildSchemaIndexes(schemas)
	if err != nil {
		return nil, apperrors.NewInternalError("failed to build object identity indexes", err)
	}
	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,
		objectByKeyPrefixMap:  objectByKeyPrefixMap,
		keyPrefixByObjectMap:  keyPrefixByObjectMap,
		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 in one batch.
	validationRulesMap := make(map[string][]*models.ValidationRule)

	stepStart = time.Now()
	log.Println("  [3/5] Loading validation rules...")
	allRules, err := ms.repo.GetAllValidationRules(ctx)
	if err != nil {
		return apperrors.NewInternalError("failed to load validation rules", err)
	}
	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))

	// 4. 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,
		objectByKeyPrefixMap:  coreSnap.objectByKeyPrefixMap,
		keyPrefixByObjectMap:  coreSnap.keyPrefixByObjectMap,
		relationshipsMap:      coreSnap.relationshipsMap,
		childRelationshipsMap: coreSnap.childRelationshipsMap,
		flowDefinitions:       flowDefs,
		flowDefinitionMap:     flowDefMap,
		validationRulesMap:    validationRulesMap,
	}
	ms.cache.Store(snap)
	ms.coreCache.Store(snap)

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

// 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 ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return nil
		}
		schema, _ := requestCachedRuntimeSchema(ctx, view, apiName)
		return schema
	}
	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.clearTriggerSubscriptionRuntimeCache()
	ms.notifyCacheInvalidationHooks()
	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.clearTriggerSubscriptionRuntimeCache()
	ms.notifyCacheInvalidationHooks()
	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 {
	if ms.UsesRequestBoundRuntimeView() {
		// This legacy context-free getter cannot safely select a Runtime view.
		// New Runtime capabilities must use context-bearing metadata methods.
		return nil
	}
	// 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
}

func (ms *MetadataService) GetSchemas(ctx context.Context) []*models.ObjectMetadata {
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return []*models.ObjectMetadata{}
		}
		return view.Objects()
	}
	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{}
}

// RollupChildDependencies returns the metadata-owned rollup fields affected by
// writes to childObjectAPIName. Independent Runtime views keep a narrow index
// over their immutable artifact, so transaction planning does not reconstruct
// every unrelated object and field. The legacy composition retains its
// process-wide schema-cache behavior.
func (ms *MetadataService) RollupChildDependencies(ctx context.Context, childObjectAPIName string) []domainSchema.RollupChildDependency {
	if ms == nil {
		return nil
	}
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return nil
		}
		return view.RollupChildDependencies(childObjectAPIName)
	}
	return domainSchema.FindRollupChildDependencies(ms.GetSchemas(ctx), childObjectAPIName)
}

func (ms *MetadataService) HasRollupChildDependencies(ctx context.Context, childObjectAPIName string) bool {
	if ms == nil {
		return false
	}
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		return ok && view != nil && view.HasRollupChildDependencies(childObjectAPIName)
	}
	return domainSchema.HasRollupChildDependencies(ms.GetSchemas(ctx), childObjectAPIName)
}

// HasAnySiblingMaxDefaults preserves the legacy transaction-safety question
// without forcing independent Runtime callers to reconstruct the complete
// metadata view. The legacy composition scans its process-cached schemas;
// Runtime consumes the immutable view's precomputed fact.
func (ms *MetadataService) HasAnySiblingMaxDefaults(ctx context.Context) bool {
	if ms == nil {
		return false
	}
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		return ok && view != nil && view.HasAnySiblingMaxDefaults()
	}
	for _, schema := range ms.GetSchemas(ctx) {
		if schema == nil {
			continue
		}
		for i := range schema.Fields {
			parent := schema.Fields[i].SiblingMaxParentField
			if parent != nil && strings.TrimSpace(*parent) != "" {
				return true
			}
		}
	}
	return false
}

// 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 searchmetadata.IsUserSearchableObject(schema) {
			searchable = append(searchable, schema.APIName)
		}
	}
	return searchable
}

// GetSearchScopeObjects returns the ordered object list for the global-search
// scope dropdown. Installed pack declarations are authoritative when present;
// otherwise the service falls back to all searchable objects from metadata.
func (ms *MetadataService) GetSearchScopeObjects(ctx context.Context) []string {
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("\u26a0\ufe0f Failed to initialize core cache in GetSearchScopeObjects: %v", err)
		return []string{}
	}

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

	searchableByKey := make(map[string]string)
	searchable := make([]string, 0)
	for _, schema := range snap.schemas {
		if !searchmetadata.IsUserSearchableObject(schema) {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(schema.APIName))
		if key == "" {
			continue
		}
		searchableByKey[key] = schema.APIName
		searchable = append(searchable, schema.APIName)
	}

	curated := PackSearchScopeObjectNames()
	if len(curated) == 0 {
		return searchable
	}

	ordered := make([]string, 0, len(searchable))
	seen := make(map[string]struct{}, len(curated))
	for _, objectAPIName := range curated {
		key := strings.ToLower(strings.TrimSpace(objectAPIName))
		canonical, ok := searchableByKey[key]
		if !ok {
			continue
		}
		if _, dup := seen[key]; dup {
			continue
		}
		seen[key] = struct{}{}
		ordered = append(ordered, canonical)
	}
	if len(ordered) == 0 {
		return searchable
	}

	for _, objectAPIName := range searchable {
		key := strings.ToLower(strings.TrimSpace(objectAPIName))
		if key == "" {
			continue
		}
		if _, dup := seen[key]; dup {
			continue
		}
		seen[key] = struct{}{}
		ordered = append(ordered, objectAPIName)
	}

	return ordered
}

// PackSearchScopeObjectNames returns the installed-pack-authored dropdown order
// without adding searchable metadata fallback objects. It intentionally mirrors
// the package-level runtime cache for callers that need the curated list itself.
func (ms *MetadataService) PackSearchScopeObjectNames(context.Context) []string {
	if ms == nil {
		return []string{}
	}
	return PackSearchScopeObjectNames()
}

func IsUserSearchableObject(schema *models.ObjectMetadata) bool {
	return searchmetadata.IsUserSearchableObject(schema)
}

// 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)
		}
	}
}
