package metadata

import (
	"context"
	"fmt"
	"strings"
	"sync"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/models"
)

// runtimeMetadataViewContextKey is owned by the metadata module so every
// independently composed Runtime execution path (request or durable worker)
// binds the same immutable view through one read seam. The value never crosses
// a process boundary; durable work persists the content digests needed to
// reconstruct it.
type runtimeMetadataViewContextKey struct{}
type runtimeFlowWorkViewContextKey struct{}
type runtimeMetadataReadCacheContextKey struct{}

type runtimeMetadataReadCacheKey struct {
	view    *catalogsnapshot.RuntimeMetadataView
	apiName string
}

type runtimeMetadataReadCacheEntry struct {
	schema *models.ObjectMetadata
	found  bool
}

// runtimeMetadataReadCache restores the old in-process MetadataService read
// shape at the correct lifetime: one admitted request. Independent Runtime
// artifacts deliberately expose detached mutable models, but engine callers
// historically treat MetadataService schemas as read-only and may ask for the
// same object hundreds of times while executing one Flow. Keeping the first
// detached materialization only in the request context avoids both repeated
// full-object copies and cross-request mutation leakage.
type runtimeMetadataReadCache struct {
	mu             sync.Mutex
	schemas        map[runtimeMetadataReadCacheKey]runtimeMetadataReadCacheEntry
	formulaEngines map[*catalogsnapshot.RuntimeMetadataView]runtimeFormulaEngineCacheEntry
}

type runtimeFormulaEngineCacheEntry struct {
	engine *formula.Engine
	err    error
}

// WithRuntimeMetadataReadCache binds an empty request-local schema cache. It
// carries no authority: every lookup is still resolved from the exact Runtime
// view selected by the existing resolver and keyed by that view pointer.
func WithRuntimeMetadataReadCache(ctx context.Context) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	if cache, ok := ctx.Value(runtimeMetadataReadCacheContextKey{}).(*runtimeMetadataReadCache); ok && cache != nil {
		return ctx
	}
	return context.WithValue(ctx, runtimeMetadataReadCacheContextKey{}, &runtimeMetadataReadCache{
		schemas:        make(map[runtimeMetadataReadCacheKey]runtimeMetadataReadCacheEntry),
		formulaEngines: make(map[*catalogsnapshot.RuntimeMetadataView]runtimeFormulaEngineCacheEntry),
	})
}

func requestCachedRuntimeSchema(
	ctx context.Context,
	view *catalogsnapshot.RuntimeMetadataView,
	apiName string,
) (*models.ObjectMetadata, bool) {
	if ctx == nil || view == nil {
		return nil, false
	}
	cache, ok := ctx.Value(runtimeMetadataReadCacheContextKey{}).(*runtimeMetadataReadCache)
	if !ok || cache == nil {
		return view.Object(apiName)
	}
	// Match catalogsnapshot.foldIdentity exactly. Trimming here would let an
	// invalid spaced identity cache a miss under the same key as a later valid
	// identity and would turn the optimization into a request-local semantic
	// change.
	key := runtimeMetadataReadCacheKey{view: view, apiName: strings.ToLower(apiName)}
	cache.mu.Lock()
	defer cache.mu.Unlock()
	if entry, exists := cache.schemas[key]; exists {
		return entry.schema, entry.found
	}
	schema, found := view.Object(apiName)
	cache.schemas[key] = runtimeMetadataReadCacheEntry{schema: schema, found: found}
	return schema, found
}

// WithRuntimeMetadataView binds one already verified immutable view to ctx.
// Callers must verify artifact scope, lineage, and local compatibility before
// binding it. A nil view deliberately clears no prior value and fails closed at
// RuntimeMetadataViewFromContext.
func WithRuntimeMetadataView(ctx context.Context, view *catalogsnapshot.RuntimeMetadataView) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	// Every bound immutable view is also a formula-vocabulary authority. Keep
	// its derived engine request-local by construction so API and worker
	// callers cannot accidentally turn formula evaluation into per-record
	// compilation merely by omitting a separate cache decorator.
	ctx = WithRuntimeMetadataReadCache(ctx)
	return context.WithValue(ctx, runtimeMetadataViewContextKey{}, view)
}

// RuntimeMetadataViewFromContext is the sole neutral resolver used by Runtime
// metadata consumers in both the API and worker compositions.
func RuntimeMetadataViewFromContext(ctx context.Context) (*catalogsnapshot.RuntimeMetadataView, bool) {
	if ctx == nil {
		return nil, false
	}
	view, ok := ctx.Value(runtimeMetadataViewContextKey{}).(*catalogsnapshot.RuntimeMetadataView)
	return view, ok && view != nil
}

// RuntimeFormulaEngineFromContext resolves the formula vocabulary from the
// exact immutable Runtime view admitted for this request. The request-local
// cache owns only the compiled engine; the Runtime view remains the sole
// authority for the selected function declarations.
func RuntimeFormulaEngineFromContext(ctx context.Context) (*formula.Engine, bool, error) {
	view, ok := RuntimeMetadataViewFromContext(ctx)
	if !ok {
		return nil, false, nil
	}
	return RuntimeFormulaEngineForView(ctx, view)
}

// RuntimeFormulaEngineForView resolves one formula engine for an exact
// request-bound immutable view. The supplied view is explicit because serving
// compositions may bind a mutation sibling alongside a different primary
// capability view. Reuse never crosses the admitted request cache or the view
// pointer, so two Catalog revisions cannot share executable formula authority.
func RuntimeFormulaEngineForView(
	ctx context.Context,
	view *catalogsnapshot.RuntimeMetadataView,
) (*formula.Engine, bool, error) {
	if ctx == nil || view == nil {
		return nil, false, nil
	}
	cache, cached := ctx.Value(runtimeMetadataReadCacheContextKey{}).(*runtimeMetadataReadCache)
	if !cached || cache == nil {
		engine, err := view.NewFormulaEngine()
		return engine, true, err
	}
	cache.mu.Lock()
	defer cache.mu.Unlock()
	if entry, exists := cache.formulaEngines[view]; exists {
		return entry.engine, true, entry.err
	}
	engine, err := view.NewFormulaEngine()
	cache.formulaEngines[view] = runtimeFormulaEngineCacheEntry{engine: engine, err: err}
	return engine, true, err
}

// WithRuntimeFlowWorkView binds the already verified Flow-capability view used
// only to derive timed Flow work from a record-event transaction. Composition
// must prove that it shares the primary view's installation and Catalog
// lineage before binding it.
func WithRuntimeFlowWorkView(ctx context.Context, view *catalogsnapshot.RuntimeMetadataView) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	return context.WithValue(ctx, runtimeFlowWorkViewContextKey{}, view)
}

// RuntimeFlowWorkViewFromContext has no Catalog lookup, current/latest selector
// or fallback. It returns only the exact auxiliary view already captured at
// request admission and persisted with durable work.
func RuntimeFlowWorkViewFromContext(ctx context.Context) (*catalogsnapshot.RuntimeMetadataView, bool) {
	if ctx == nil {
		return nil, false
	}
	view, ok := ctx.Value(runtimeFlowWorkViewContextKey{}).(*catalogsnapshot.RuntimeMetadataView)
	return view, ok && view != nil
}

// requireRequestBoundRuntimeView returns the immutable view only when it owns
// the exact capability profile required by an execution metadata read. An
// empty result is a valid metadata fact; a missing or wrong-profile authority
// is not, and must never be collapsed into that empty result.
func (ms *MetadataService) requireRequestBoundRuntimeView(
	ctx context.Context,
	profile catalogsnapshot.ProjectionProfile,
	purpose string,
) (*catalogsnapshot.RuntimeMetadataView, error) {
	if ms == nil || !ms.UsesRequestBoundRuntimeView() {
		return nil, fmt.Errorf("request-bound Runtime metadata service is required for %s", purpose)
	}
	view, ok := ms.runtimeView(ctx)
	if !ok || view == nil {
		return nil, fmt.Errorf("request-bound Runtime metadata view is required for %s", purpose)
	}
	if view.Profile() != profile {
		return nil, fmt.Errorf("request-bound Runtime metadata view profile %q cannot provide %s", view.Profile(), purpose)
	}
	return view, nil
}

// requireRequestBoundRuntimeMutationView accepts only compiler-validated
// mutation closures. This keeps Approval execution on its own explicit
// profile while allowing the existing mutation/Flow machinery to consume the
// exact facts already projected into that request-bound artifact.
func (ms *MetadataService) requireRequestBoundRuntimeMutationView(
	ctx context.Context,
	purpose string,
) (*catalogsnapshot.RuntimeMetadataView, error) {
	if ms == nil || !ms.UsesRequestBoundRuntimeView() {
		return nil, fmt.Errorf("request-bound Runtime metadata service is required for %s", purpose)
	}
	view, ok := ms.runtimeView(ctx)
	if !ok || view == nil {
		return nil, fmt.Errorf("request-bound Runtime metadata view is required for %s", purpose)
	}
	if !view.SupportsMutationClosure() {
		return nil, fmt.Errorf("request-bound Runtime metadata view profile %q cannot provide %s", view.Profile(), purpose)
	}
	return view, nil
}
