package automation

import (
	"context"
	"errors"
	"fmt"
	"strings"

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

type flowFormulaResourceScopeKey struct{}

type flowFormulaResourceScope struct {
	resources    map[string]models.FlowFormula
	deps         map[string][]string
	parseErrors  map[string]error
	namespaceErr error
}

func withFlowFormulaResourceScope(ctx context.Context, interview *models.FlowInterview, resources []models.FlowFormula) context.Context {
	scope := &flowFormulaResourceScope{
		resources:   make(map[string]models.FlowFormula, len(resources)),
		deps:        make(map[string][]string, len(resources)),
		parseErrors: make(map[string]error),
	}
	for _, resource := range resources {
		name := strings.TrimSpace(resource.Name)
		if name == "" {
			continue
		}
		if _, exists := scope.resources[name]; exists {
			scope.namespaceErr = fmt.Errorf("flow formula resource %q is declared more than once", name)
			break
		}
		scope.resources[name] = resource
		// Persisted interviews created by the old eager implementation may
		// contain serialized Formula results. They are derived values, never
		// authoritative state, so discard them at every entry/resume boundary.
		if interview != nil && interview.ContextData != nil {
			delete(interview.ContextData, name)
		}
	}
	if scope.namespaceErr == nil {
		for name, resource := range scope.resources {
			identifiers, err := formula.CollectExpressionIdentifiers(resource.Expression)
			if err != nil {
				scope.parseErrors[name] = fmt.Errorf("parse flow formula resource %q dependencies: %w", name, err)
				continue
			}
			for _, identifier := range identifiers {
				if _, ok := scope.resources[identifier]; ok {
					scope.deps[name] = append(scope.deps[name], identifier)
				}
			}
		}
	}
	return context.WithValue(ctx, flowFormulaResourceScopeKey{}, scope)
}

func flowFormulaResourcesFromContext(ctx context.Context) *flowFormulaResourceScope {
	if ctx == nil {
		return nil
	}
	scope, _ := ctx.Value(flowFormulaResourceScopeKey{}).(*flowFormulaResourceScope)
	return scope
}

// ResolveFlowResource is the single error-aware read path for runtime Flow
// elementReference values. Formula Resources are evaluated on each top-level
// access against the current interview state; recursive dependencies memoize
// only within that one access.
func (e *FlowExecutor) ResolveFlowResource(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, value interface{}) (interface{}, bool, error) {
	ref, isReference := flowElementReferenceName(value)
	if isReference {
		root := ref
		if dot := strings.IndexByte(root, '.'); dot >= 0 {
			root = root[:dot]
		}
		if scope := flowFormulaResourcesFromContext(ctx); scope != nil {
			if _, ok := scope.resources[root]; ok {
				resolved, err := e.evaluateFlowFormulaResource(ctx, interview, user, scope, root, map[string]interface{}{}, map[string]bool{})
				if err != nil {
					return nil, false, err
				}
				if ref != root {
					// Object-valued Formula Resources were traversable when eager
					// results lived in ContextData. Preserve that contract without
					// materializing the derived value into interview state: expose the
					// evaluated root only to the existing nested-reference walker for
					// this one top-level access.
					formulaInterview := &models.FlowInterview{
						ContextData: map[string]interface{}{root: resolved},
					}
					value, present := e.resolveNestedReferenceCtxWithPresence(ctx, formulaInterview, user, ref, nil)
					return value, present, nil
				}
				return resolved, true, nil
			}
		}
	}
	resolved, present := e.resolveNonFormulaValueCtxWithPresence(ctx, interview, user, value)
	return resolved, present, nil
}

func flowElementReferenceName(value interface{}) (string, bool) {
	if typed, ok := value.(map[string]interface{}); ok {
		ref, ok := typed["elementReference"].(string)
		return strings.TrimSpace(ref), ok && strings.TrimSpace(ref) != ""
	}
	ref, ok := value.(string)
	if !ok || !looksLikeVariableReference(ref) {
		return "", false
	}
	return strings.TrimSpace(ref), true
}

func (e *FlowExecutor) evaluateFlowFormulaResource(
	ctx context.Context,
	interview *models.FlowInterview,
	user *models.UserSession,
	scope *flowFormulaResourceScope,
	name string,
	memo map[string]interface{},
	visiting map[string]bool,
) (interface{}, error) {
	if scope == nil {
		return nil, fmt.Errorf("flow formula resource scope is unavailable")
	}
	if scope.namespaceErr != nil {
		return nil, scope.namespaceErr
	}
	if value, ok := memo[name]; ok {
		return value, nil
	}
	if visiting[name] {
		return nil, fmt.Errorf("flow formula resource dependency is circular at %q", name)
	}
	resource, ok := scope.resources[name]
	if !ok {
		return nil, fmt.Errorf("flow formula resource %q is not declared", name)
	}
	if err := scope.parseErrors[name]; err != nil {
		return nil, err
	}
	visiting[name] = true
	defer delete(visiting, name)
	for _, dependency := range scope.deps[name] {
		value, err := e.evaluateFlowFormulaResource(ctx, interview, user, scope, dependency, memo, visiting)
		if err != nil {
			return nil, err
		}
		memo[dependency] = value
	}
	contextData := make(map[string]interface{})
	if interview != nil {
		for key, value := range interview.ContextData {
			if _, derived := scope.resources[key]; !derived {
				contextData[key] = value
			}
		}
	}
	for dependency, value := range memo {
		contextData[dependency] = value
	}
	fCtx := buildFlowFormulaContext(contextData, e.buildFormulaFetcher(ctx, interview, user, ""), user)
	fCtx.Variables[constants.FormulaContextSystemContext] = constants.FormulaSystemContextFromContext(ctx)
	if objectType := flowRuntimeContextFromCtx(ctx).ObjectType(); objectType != "" {
		fCtx.Schema = e.lookupObjectSchema(ctx, objectType)
	}
	e.injectFormulaPermissions(ctx, fCtx.Variables, resource.Expression, user)
	engine, err := e.flowFormulaEngine(ctx)
	if err != nil {
		return nil, fmt.Errorf("resolve flow formula authority: %w", err)
	}
	result, err := engine.Evaluate(resource.Expression, fCtx)
	if err != nil {
		// Flow Formula Resources follow the runtime data-error contract: an
		// expression that cannot produce a value for the current inputs resolves
		// to null. Keep infrastructure failures distinct. In particular, a
		// transient relationship read must not masquerade as a definitive null
		// parent and steer the interview down the wrong branch.
		if errors.Is(err, formula.ErrRelationFetch) {
			return nil, fmt.Errorf("evaluate flow formula resource %q: %w", name, err)
		}
		if e != nil && e.logger != nil {
			e.logger.Warn(fmt.Sprintf(
				"flow: formula resource %q evaluation failed; using null: %s",
				name, err.Error(),
			))
		}
		result = nil
	}
	memo[name] = result
	return result, nil
}

func (e *FlowExecutor) flowFormulaEngine(ctx context.Context) (*formula.Engine, error) {
	if engine, bound, err := metadata.RuntimeFormulaEngineFromContext(ctx); bound {
		if err != nil {
			return nil, err
		}
		return engine, nil
	}
	if e != nil && e.formulaEngine != nil {
		return e.formulaEngine, nil
	}
	return formula.NewEngine(), nil
}
