package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.AllocationFactPropagator;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.VirtualAccessorConstResolver;
import click.kamil.springstatemachineexporter.analysis.flow.eventid.EventIdentityResolver;
import click.kamil.springstatemachineexporter.analysis.flow.eventid.EventIdentityNormalize;
import click.kamil.springstatemachineexporter.analysis.flow.eventid.EventIdentityPolyPublish;
import click.kamil.springstatemachineexporter.analysis.flow.eventid.ExpandConsult;
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
import click.kamil.springstatemachineexporter.analysis.pipeline.EventArgumentSelector;
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
import click.kamil.springstatemachineexporter.analysis.pipeline.LibraryUnwrapRegistry;
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.DynamicTriggerExpressions;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;

import java.util.*;

@Slf4j
public abstract class AbstractCallGraphEngine implements CallGraphEngine {
    protected final CodebaseContext context;
    protected final ConstantResolver constantResolver;

    protected final TypeResolver typeResolver;
    protected final CallGraphPathFinder pathFinder;
    protected final VariableTracer variableTracer;
    protected final ConstantExtractor constantExtractor;
    protected final ConstructorAnalyzer constructorAnalyzer;
    protected final ExpressionAccessClassifier expressionAccessClassifier;
    protected final AccessorResolver accessorResolver;
    protected final FieldInitializerFinder fieldInitializerFinder;
    protected final PathBindingEvaluator pathBindingEvaluator;
    protected final AllocationFactPropagator allocationFactPropagator;
    protected final VirtualAccessorConstResolver virtualAccessorConstResolver;
    protected final EventIdentityResolver eventIdentityResolver;
    private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
    private final Map<String, List<String>> polymorphicCallCache = new HashMap<>();
    private final Map<String, Map<String, String>> pathBindingsCache = new HashMap<>();
    protected Map<String, List<CallEdge>> graph;

    private ASTNode parseExpressionString(String expr) {
        if (expr == null) {
            return null;
        }
        return parsedNodeCache.computeIfAbsent(expr, click.kamil.springstatemachineexporter.ast.common.AstUtils::parseExpression);
    }

    @FunctionalInterface
    protected interface RhsResolver {
        String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
    }

    public AbstractCallGraphEngine(CodebaseContext context) {
        this.context = context;
        this.constantResolver = context.getConstantResolver();

        this.typeResolver = new TypeResolver(context);
        this.pathFinder = new CallGraphPathFinder(context);
        this.variableTracer = new VariableTracer(context, constantResolver);
        this.constantExtractor = new ConstantExtractor(context, constantResolver, this::resolveCalledMethod);
        this.constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);

        // Wire up cross dependencies
        this.variableTracer.setConstantExtractor(this.constantExtractor);
        this.variableTracer.setTypeResolver(this.typeResolver);
        this.constantExtractor.setVariableTracer(this.variableTracer);
        this.constantExtractor.setConstructorAnalyzer(this.constructorAnalyzer);
        this.constructorAnalyzer.setVariableTracer(this.variableTracer);
        this.constructorAnalyzer.setConstantExtractor(this.constantExtractor);
        this.expressionAccessClassifier = new ExpressionAccessClassifier(
                context, variableTracer, constantResolver, this::parseExpressionString);
        this.constantExtractor.setExpressionAccessClassifier(this.expressionAccessClassifier);
        this.accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
        this.constantExtractor.setAccessorResolver(this.accessorResolver);
        this.fieldInitializerFinder = new FieldInitializerFinder(context);
        this.pathBindingEvaluator = new PathBindingEvaluator(context, variableTracer, constantResolver, typeResolver);
        this.allocationFactPropagator = new AllocationFactPropagator(
                context, variableTracer, constructorAnalyzer);
        this.virtualAccessorConstResolver = new VirtualAccessorConstResolver(
                context, accessorResolver, constantResolver);
        this.eventIdentityResolver = new EventIdentityResolver(
                context,
                allocationFactPropagator,
                virtualAccessorConstResolver,
                variableTracer,
                constantResolver);
    }

    public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
        accessorResolver.clearSessionCache();
        constructorAnalyzer.clearReturnedCicCache();
        variableTracer.clearAnalysisCaches();
        polymorphicCallCache.clear();
        context.clearAnalysisCaches();
        pathFinder.clearAnalysisCaches();
        pathBindingEvaluator.clearAnalysisCaches();
        pathBindingsCache.clear();
        parsedNodeCache.clear();
        if (eventIdentityResolver != null) {
            eventIdentityResolver.clearSessionCache();
        }
        Map<String, List<CallEdge>> callGraph = buildCallGraph();
        List<CallChain> chains = new ArrayList<>();

        for (EntryPoint ep : entryPoints) {
            List<MessagingStartContext> startContexts = resolveMessagingStartContexts(ep, callGraph);

            List<Map<String, String>> bindingVariants =
                    EntryPointBindingExpander.expandEntryPointBindings(ep, context, callGraph);
            if (bindingVariants.isEmpty()) {
                bindingVariants = List.of(Map.of());
            }

            for (Map<String, String> initialBindings : bindingVariants) {
                EntryPoint chainEntryPoint = initialBindings.isEmpty()
                        ? ep
                        : EntryPointBindingExpander.withResolvedPath(ep, initialBindings);

                boolean foundAny = false;
                for (TriggerPoint tp : triggers) {
                    String targetMethod = tp.getClassName() + "." + tp.getMethodName();
                    List<List<String>> allPaths = new ArrayList<>();
                    for (MessagingStartContext startContext : startContexts) {
                        if (initialBindings.isEmpty()) {
                            allPaths.addAll(pathFinder.findAllPaths(
                                    startContext.startMethod(),
                                    targetMethod,
                                    callGraph,
                                    new HashSet<>(),
                                    startContext.concreteSelfFqn()));
                        } else {
                            allPaths.addAll(pathFinder.findAllPaths(
                                    startContext.startMethod(),
                                    targetMethod,
                                    callGraph,
                                    new HashSet<>(),
                                    pathBindingEvaluator,
                                    initialBindings,
                                    startContext.concreteSelfFqn()));
                        }
                    }
                    Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
                    for (List<String> path : uniquePaths) {
                        boolean compatible = initialBindings.isEmpty()
                                ? pathBindingEvaluator.isPathCompatible(path, callGraph, pathFinder)
                                : pathBindingEvaluator.isPathCompatible(path, callGraph, pathFinder, initialBindings);
                        if (!compatible) {
                            continue;
                        }
                        foundAny = true;
                        Map<String, String> pathBindings = resolvePathBindings(path, callGraph, initialBindings);
                        TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph, initialBindings);
                        Map<String, String> constraintBindings =
                                !pathBindings.isEmpty() ? pathBindings : initialBindings;
                        if (resolvedTp != null
                                && resolvedTp.getConstraint() != null
                                && constraintBindings != null
                                && !constraintBindings.isEmpty()
                                && CarrierConstraintSupport.contradictsCarrierBindings(
                                        resolvedTp.getConstraint(), constraintBindings)) {
                            continue;
                        }
                        if (resolvedTp != null) {
                            resolvedTp = TriggerMachineTypeRebinder.rebind(resolvedTp, path, context);
                            String contextMachineId = pathFinder.extractContextMachineId(path, callGraph);
                            Map<String, String> exportedBindings = !pathBindings.isEmpty()
                                    ? pathBindings
                                    : (initialBindings == null || initialBindings.isEmpty()
                                            ? Map.of()
                                            : initialBindings);
                            chains.add(CallChain.builder()
                                    .entryPoint(chainEntryPoint)
                                    .triggerPoint(resolvedTp)
                                    .methodChain(path)
                                    .contextMachineId(contextMachineId)
                                    .pathBindings(exportedBindings.isEmpty() ? null : Map.copyOf(exportedBindings))
                                    .build());
                        }
                    }
                }
                if (!foundAny && log.isDebugEnabled()) {
                    log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}",
                            chainEntryPoint.getName(),
                            startContexts.stream().map(MessagingStartContext::startMethod).toList(),
                            callGraph.keySet());
                }
            }
        }
        return chains;
    }

    /**
     * Messaging entries annotated on abstract/interface types must be searched once per concrete
     * runtime listener so template-method pruning has a concrete {@code concreteSelf}.
     * REST/CUSTOM entries keep a single start at the declaring type.
     */
    private List<MessagingStartContext> resolveMessagingStartContexts(
            EntryPoint ep,
            Map<String, List<CallEdge>> callGraph) {
        String declaringClass = ep.getClassName();
        String methodName = ep.getMethodName();
        String declaringStart = declaringClass + "." + methodName;

        if (!isMessagingEntryType(ep.getType()) || declaringClass == null || methodName == null) {
            return List.of(new MessagingStartContext(declaringStart, declaringClass));
        }

        if (context.isConcreteClassType(declaringClass)) {
            return List.of(new MessagingStartContext(declaringStart, declaringClass));
        }

        List<String> impls = context.getImplementations(declaringClass);
        if (impls == null || impls.isEmpty()) {
            return List.of(new MessagingStartContext(declaringStart, declaringClass));
        }

        LinkedHashSet<MessagingStartContext> contexts = new LinkedHashSet<>();
        for (String implFqn : impls) {
            if (!context.isConcreteClassType(implFqn)) {
                continue;
            }
            String implStart = implFqn + "." + methodName;
            String startMethod = callGraph.containsKey(implStart) ? implStart : declaringStart;
            contexts.add(new MessagingStartContext(startMethod, implFqn));
        }

        if (contexts.isEmpty()) {
            return List.of(new MessagingStartContext(declaringStart, declaringClass));
        }
        return List.copyOf(contexts);
    }

    private static boolean isMessagingEntryType(EntryPoint.Type type) {
        return type == EntryPoint.Type.JMS
                || type == EntryPoint.Type.RABBIT
                || type == EntryPoint.Type.KAFKA
                || type == EntryPoint.Type.SQS
                || type == EntryPoint.Type.SNS;
    }

    private record MessagingStartContext(String startMethod, String concreteSelfFqn) {
    }

    protected TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
        return resolveTriggerPointParameters(tp, path, callGraph, Map.of());
    }

    protected Map<String, String> resolvePathBindings(
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            Map<String, String> initialBindings) {
        if (path == null || path.size() < 2) {
            return initialBindings != null ? Map.copyOf(initialBindings) : Map.of();
        }
        String cacheKey = pathBindingsCacheKey(path, initialBindings);
        return pathBindingsCache.computeIfAbsent(cacheKey, ignored -> {
            Map<String, String> traced = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
            return mergeBindings(initialBindings, traced);
        });
    }

    private static String pathBindingsCacheKey(List<String> path, Map<String, String> initialBindings) {
        StringJoiner joiner = new StringJoiner("\0");
        for (String hop : path) {
            joiner.add(hop);
        }
        if (initialBindings != null) {
            initialBindings.entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .forEach(entry -> joiner.add(entry.getKey() + "=" + entry.getValue()));
        }
        return joiner.toString();
    }

    protected TriggerPoint resolveTriggerPointParameters(
            TriggerPoint tp,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            Map<String, String> initialBindings) {
        if (path.size() < 2) {
            boolean isExternal = isExternalParameter(tp.getClassName() + "." + tp.getMethodName(), tp.getEvent());
            return tp.toBuilder().external(isExternal).constraint(tp.getConstraint()).build();
        }
        
        String event = tp.getEvent();
        if (event == null || event.isEmpty() || event.contains("\"")) {
            return tp;
        }

        String[] finalParamNameRef = { event };
        TriggerPoint resolved = resolveTriggerPointParametersOriginal(tp, path, callGraph, finalParamNameRef, initialBindings);
        if (resolved == null) return null;
        String entryMethod = path.isEmpty() ? (tp.getClassName() + "." + tp.getMethodName()) : path.get(0);
        boolean isExternal = isExternalTrigger(entryMethod, finalParamNameRef[0], path, callGraph, initialBindings);
        
        String pathConstraint = extractPathConstraints(path, callGraph, initialBindings);
        String triggerConstraint = resolved.getConstraint();
        String finalConstraint = mergeConstraints(pathConstraint, triggerConstraint);
        
        return resolved.toBuilder().external(isExternal).constraint(finalConstraint).build();
    }

    private static String mergeConstraints(String pathConstraint, String triggerConstraint) {
        if (pathConstraint != null && triggerConstraint != null) {
            return pathConstraint + " && " + triggerConstraint;
        }
        if (pathConstraint != null) {
            return pathConstraint;
        }
        return triggerConstraint;
    }

    private static Expression selectTernaryBranch(ConditionalExpression cond, Map<String, String> bindings) {
        if (cond == null || bindings == null || bindings.isEmpty()) {
            return null;
        }
        Expression condition = cond.getExpression();
        if (condition instanceof PrefixExpression prefixExpression
                && prefixExpression.getOperator() == PrefixExpression.Operator.NOT
                && prefixExpression.getOperand() instanceof SimpleName negatedName) {
            String bound = bindings.get(negatedName.getIdentifier());
            if ("true".equals(bound)) {
                return cond.getElseExpression();
            }
            if ("false".equals(bound)) {
                return cond.getThenExpression();
            }
            return null;
        }
        if (!(condition instanceof SimpleName simpleName)) {
            return null;
        }
        String bound = bindings.get(simpleName.getIdentifier());
        if ("true".equals(bound)) {
            return cond.getThenExpression();
        }
        if ("false".equals(bound)) {
            return cond.getElseExpression();
        }
        return null;
    }

    protected TriggerPoint resolveTriggerPointParametersOriginal(
            TriggerPoint tp,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            String[] finalParamNameRef) {
        return resolveTriggerPointParametersOriginal(tp, path, callGraph, finalParamNameRef, Map.of());
    }

    protected TriggerPoint resolveTriggerPointParametersOriginal(
            TriggerPoint tp,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            String[] finalParamNameRef,
            Map<String, String> initialBindings) {
        click.kamil.springstatemachineexporter.ast.common.CodebaseContext contextRef = context;
        contextRef.setAnalysisCallPath(path);
        final boolean debug = log.isDebugEnabled();
        try {
            String event = tp.getEvent();
            String triggerScope = path.isEmpty() ? null : path.get(path.size() - 1);
            event = unwrapConverterMethods(event, triggerScope);
            String currentParamName = event;
            String resolvedValue = event;
            String methodSuffix = "";
            String scopeMethod = path.isEmpty() ? null : path.get(0);
            String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix, scopeMethod);
            currentParamName = extractedEntry[0];
            methodSuffix = extractedEntry[1];
            boolean isAmbiguous = false;
            if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName;
        
        for (int i = path.size() - 1; i > 0; i--) {
            String target = path.get(i);
            String caller = path.get(i - 1);
            boolean allowSyntheticPlaceholder = i == path.size() - 1;
            int paramIndex = typeResolver.getParameterIndex(target, currentParamName, allowSyntheticPlaceholder);
            if (paramIndex < 0) {
                if ("this".equals(currentParamName)) {
                    String actualReceiver = null;
                    for (int j = i; j > 0; j--) {
                        String t = path.get(j);
                        String c = path.get(j - 1);
                        List<CallEdge> edges = callGraph.get(c);
                        if (edges != null) {
                            for (CallEdge edge : edges) {
                                if (edge.getTargetMethod().equals(t) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), t)) {
                                    if (edge.getReceiver() != null && !edge.getReceiver().isEmpty()) {
                                        actualReceiver = edge.getReceiver();
                                        break;
                                    }
                                }
                            }
                        }
                        if (actualReceiver != null) break;
                    }
                    if (actualReceiver != null) {
                        currentParamName = actualReceiver;
                        resolvedValue = actualReceiver + methodSuffix;
                        if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName;
                        paramIndex = typeResolver.getParameterIndex(target, currentParamName);
                    }
                }
                
                if (paramIndex < 0) {
                    Map<String, String> parameterValues = mergeBindings(
                            initialBindings,
                            variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i));
                    String tracedVar = variableTracer.traceLocalVariable(target, currentParamName, parameterValues);
                    if (tracedVar != null && !tracedVar.equals(currentParamName)) {
                        String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix, target);
                        tracedVar = extractedTraced[0];
                        methodSuffix = extractedTraced[1];
                        currentParamName = tracedVar;
                        resolvedValue = tracedVar + methodSuffix;
                        paramIndex = typeResolver.getParameterIndex(target, currentParamName);
                    }
                }
            }
            if (paramIndex < 0) {
                break; 
            }
            
            List<CallEdge> edges = callGraph.get(caller);
            boolean found = false;
            if (edges != null) {
                for (CallEdge edge : edges) {
                    if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
                        String expectedType = typeResolver.getParameterTypeAtCallSite(caller, target, paramIndex);
                        if (expectedType == null) {
                            expectedType = typeResolver.getParameterType(target, paramIndex);
                        }
                        if (expectedType != null && paramIndex < edge.getArguments().size()) {
                            String argValue = edge.getArguments().get(paramIndex);
                            if (!FunctionalInterfaceTypes.isProvablyResolvedCallSiteArgument(argValue, expectedType)) {
                                String actualType = null;
                                // ENUM_SET tokens contain dots but are not type FQNs — treating them
                                // as types skips the edge and leaves the bare param (→ SYMBOLIC poly).
                                if (argValue != null
                                        && !argValue.startsWith("ENUM_SET:")
                                        && argValue.contains(".")
                                        && !argValue.contains("(")) {
                                    String prefix = argValue.substring(0, argValue.lastIndexOf('.'));
                                    if (!prefix.equals("this") && !prefix.equals("super")) {
                                        actualType = prefix;
                                    }
                                }
                                if (actualType == null && argValue != null && !argValue.contains(".")) {
                                    actualType = variableTracer.getVariableDeclaredType(caller, argValue);
                                }
                                if (actualType != null && !typeResolver.isTypeCompatible(actualType, expectedType)) {
                                    continue;
                                }
                            }
                        }
                        if (paramIndex < edge.getArguments().size()) {
                            String arg = edge.getArguments().get(paramIndex);
                            if (arg != null) {
                                arg = unwrapConverterMethods(arg, caller);
                                String receiver = edge.getReceiver();
                                String actualReceiver = null;
                                if ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super.")) {
                                    if (i - 1 > 0) {
                                        String prevCaller = path.get(i - 2);
                                        List<CallEdge> prevEdges = callGraph.get(prevCaller);
                                        if (prevEdges != null) {
                                            for (CallEdge prevEdge : prevEdges) {
                                                if (prevEdge.getTargetMethod().equals(caller) || pathFinder.isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
                                                    actualReceiver = prevEdge.getReceiver();
                                                    break;
                                                }
                                            }
                                        }
                                    }
                                    if (actualReceiver == null) {
                                        actualReceiver = "this";
                                    }
                                    if ("this".equals(arg) || arg.startsWith("this.")) {
                                        arg = arg.replaceFirst("^this", actualReceiver);
                                    }
                                    // Intentionally not replacing 'super' because super should remain statically bound to the superclass of where the code is defined.
                                }
                                 String[] extractedArg = extractMethodSuffix(arg, methodSuffix, caller);
                                 arg = extractedArg[0];
                                 methodSuffix = extractedArg[1];
                                 currentParamName = arg;
                                 if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName;
                                 resolvedValue = arg + methodSuffix;
                                 found = true;
                                 break;
                            }
                        }
                    }
                }
            }
            if (!found) {
                // Bridge polymorphic interfaces to implementations without graph edges
                String callerSimple = caller.contains(".") ? caller.substring(caller.lastIndexOf('.') + 1) : caller;
                String targetSimple = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
                if (callerSimple.equals(targetSimple)) {
                    String interfaceParamName = typeResolver.getParameterName(caller, paramIndex);
                    if (interfaceParamName != null) {
                        currentParamName = interfaceParamName;
                        if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName;
                        resolvedValue = currentParamName + methodSuffix;
                        found = true;
                    }
                }
            }
            if (!found) break; 
        }
        
        String entryMethod = path.get(0);

        if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
            String getterExpression = resolvedValue + methodSuffix;
            List<String> chainedGetterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
            if (chainedGetterEvents == null || chainedGetterEvents.isEmpty()) {
                String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
                        getterExpression, entryMethod, constantResolver, context);
                if (remapped != null) {
                    chainedGetterEvents = evaluateGetterOnReceiver(remapped, "", entryMethod, path, callGraph);
                }
            }
            if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
                if (debug) {
                    log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
                }
                return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
            }

            if (expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
                List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
                if (!setterEvents.isEmpty()) {
                    if (debug) {
                        log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
                    }
                    return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
                }
            }
        }

        int entryParamIndex = typeResolver.getParameterIndex(entryMethod, currentParamName);
        if (entryParamIndex < 0) {
            if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
                List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
                if (!setterEvents.isEmpty()) {
                    if (debug) {
                        log.debug("Early return 1: setterEvents = {}", setterEvents);
                    }
                    return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
                }
            }

            boolean hasGetterOnReceiver = methodSuffix != null
                    && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)
                    && currentParamName != null
                && (currentParamName.contains("new ") || (!currentParamName.replaceFirst("^this\\.", "").contains(".") && !currentParamName.contains("(")));
            if (hasGetterOnReceiver) {
                List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
                if (getterEvents != null && !getterEvents.isEmpty()) {
                    if (debug) {
                        log.debug("Early return 2: getterEvents = {}", getterEvents);
                    }
                    return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, getterEvents);
                }
            }

            String tracedVar = variableTracer.traceLocalVariable(entryMethod, currentParamName, initialBindings);
            if (tracedVar != null && !tracedVar.equals(currentParamName)) {
                String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix, entryMethod);
                tracedVar = extractedFinalTraced[0];
                methodSuffix = extractedFinalTraced[1];
                currentParamName = tracedVar;
                if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName;
                resolvedValue = tracedVar + methodSuffix;
            }
        }
        
        if (debug) {
            log.debug("resolvedValue before ENUM_SET check: {} for path: {}", resolvedValue, path);
        }
        List<String> polymorphicEvents = new ArrayList<>();
        
        if (resolvedValue.startsWith("ENUM_SET:")) {
            for (String eVal : resolvedValue.substring(9).split(",")) {
                String parsed = constantExtractor.parseEnumSetElement(eVal);
                if (!polymorphicEvents.contains(parsed)) polymorphicEvents.add(parsed);
            }
            return TriggerPoint.builder()
                            .event(resolvedValue)
                    .className(tp.getClassName())
                    .methodName(tp.getMethodName())
                    .sourceFile(tp.getSourceFile())
                    .sourceModule(tp.getSourceModule())
                    .stateMachineId(tp.getStateMachineId())
                    .sourceState(tp.getSourceState())
                    .lineNumber(tp.getLineNumber())
                    .polymorphicEvents(polymorphicEvents)
                    .constraint(tp.getConstraint())
                    .stateTypeFqn(tp.getStateTypeFqn())
                    .eventTypeFqn(tp.getEventTypeFqn())
                    .external(tp.isExternal())
                    .ambiguous(polymorphicEvents.size() > 1)
                    .build();
        }
        
        ASTNode exprNode = parseExpressionString(resolvedValue);
        while (exprNode instanceof ParenthesizedExpression pe) {
            exprNode = pe.getExpression();
        }
        boolean[] keyedMapLookupOnInitializer = {false};
        boolean[] pathBoundMapKeyOnInitializer = {false};

        if (exprNode instanceof Expression expressionNode) {
            String remappedGetter = ReactiveExpressionSupport.remapLambdaParameterGetter(
                    expressionNode, constantResolver, context);
            if (remappedGetter == null) {
                remappedGetter = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
                        resolvedValue, entryMethod, constantResolver, context);
            }
            if (remappedGetter != null) {
                List<String> remappedEvents = evaluateGetterOnReceiver(
                        remappedGetter, "", entryMethod, path, callGraph);
                if (remappedEvents != null && !remappedEvents.isEmpty()) {
                    return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, remappedEvents);
                }
            }
            String reactivePayload = ReactiveExpressionSupport.extractPayload(
                    expressionNode, constantResolver, context);
            if (reactivePayload != null) {
                polymorphicEvents.add(reactivePayload);
                return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
            }
        }
        
        String varName = null;
        String methodName = null;
        String declaredType = null;
        String sourceMethod = null;

        if (exprNode instanceof MethodInvocation mi) {
            methodName = mi.getName().getIdentifier();
            if ("get".equals(methodName) || "getOrDefault".equals(methodName)) {
                boolean keyedLookup = expressionAccessClassifier.isKeyedLookup(mi, entryMethod);
                boolean functionalGet = "get".equals(methodName) && mi.arguments().isEmpty() && !keyedLookup;
                if (keyedLookup) {
                    List<String> keyedResults = resolveKeyedMapLookup(mi, path, callGraph);
                    if (!keyedResults.isEmpty()) {
                        polymorphicEvents.addAll(keyedResults);
                        return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
                    }
                    boolean pathBoundKey = hasPathBindingForMapKey(mi, path, callGraph);
                    if (pathBoundKey) {
                        keyedMapLookupOnInitializer[0] = true;
                        pathBoundMapKeyOnInitializer[0] = true;
                    }
                    if (!pathBoundKey) {
                        addConstantsFromTracedExpression((Expression) exprNode, polymorphicEvents, path, callGraph, entryMethod);
                        if (!polymorphicEvents.isEmpty()) {
                            return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
                        }
                    }
                } else if (functionalGet) {
                    addConstantsFromTracedExpression((Expression) exprNode, polymorphicEvents, path, callGraph, entryMethod);
                    if (!polymorphicEvents.isEmpty()) {
                        return TriggerPoint.builder()
                                .event(resolvedValue)
                                .className(tp.getClassName())
                                .methodName(tp.getMethodName())
                                .sourceFile(tp.getSourceFile())
                                .sourceModule(tp.getSourceModule())
                                .stateMachineId(tp.getStateMachineId())
                                .sourceState(tp.getSourceState())
                                .lineNumber(tp.getLineNumber())
                                .polymorphicEvents(polymorphicEvents)
                                .constraint(tp.getConstraint())
                                .stateTypeFqn(tp.getStateTypeFqn())
                                .eventTypeFqn(tp.getEventTypeFqn())
                                .external(tp.isExternal())
                                .ambiguous(isAmbiguous)
                                .build();
                    }
                }
            }
            boolean handledStaticEnum = false;
            if (methodName.equals("valueOf") && mi.arguments().size() == 1) {
                Object arg = mi.arguments().get(0);
                String enumTypeName = resolveValueOfEnumTypeName(mi);
                if (arg instanceof StringLiteral sl) {
                    String literal = sl.getLiteralValue();
                    if (literal != null && !literal.isBlank()) {
                        if (enumTypeName != null) {
                            polymorphicEvents.add(reconstructEnumRef(enumTypeName + "." + literal));
                        } else {
                            polymorphicEvents.add(reconstructEnumRef(literal));
                        }
                    }
                    methodName = null;
                    handledStaticEnum = true;
                } else if (arg instanceof QualifiedName qn) {
                    String constant = qn.getName().getIdentifier();
                    if (constant != null && !constant.isBlank()) {
                        if (enumTypeName != null) {
                            polymorphicEvents.add(reconstructEnumRef(enumTypeName + "." + constant));
                        } else {
                            polymorphicEvents.add(reconstructEnumRef(constant));
                        }
                    }
                    methodName = null;
                    handledStaticEnum = true;
                } else if (arg instanceof SimpleName sn) {
                    String resolvedConstant = resolveEnumConstantFromArgument(sn.getIdentifier(), path, callGraph);
                    if (resolvedConstant != null && !resolvedConstant.isBlank()) {
                        if (enumTypeName != null) {
                            polymorphicEvents.add(reconstructEnumRef(enumTypeName + "." + resolvedConstant));
                        } else {
                            polymorphicEvents.add(reconstructEnumRef(resolvedConstant));
                        }
                        methodName = null;
                        handledStaticEnum = true;
                    }
                }
            } else if (methodName.equals("name") && mi.arguments().isEmpty()) {
                if (mi.getExpression() instanceof QualifiedName qn) {
                    polymorphicEvents.add(qn.getName().getIdentifier());
                    methodName = null;
                    handledStaticEnum = true;
                } else if (mi.getExpression() instanceof SimpleName sn) {
                    polymorphicEvents.add(sn.getIdentifier());
                    methodName = null;
                    handledStaticEnum = true;
                }
            }
            
            if (!handledStaticEnum) {
                if (mi.getExpression() instanceof SimpleName sn) {
                varName = sn.getIdentifier();
            } else if (mi.getExpression() instanceof ParenthesizedExpression pe && 
                       pe.getExpression() instanceof CastExpression ce && 
                       ce.getExpression() instanceof SimpleName sn) {
                varName = sn.getIdentifier();
                declaredType = ce.getType().toString();
                sourceMethod = "inline-cast";
            } else if (mi.getExpression() instanceof ClassInstanceCreation cic) {
                declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
                sourceMethod = "inline-instantiation";
                
                List<String> getterResults = constructorAnalyzer.resolveInlineInstantiationGetterResult(cic, methodName, context, new java.util.HashSet<>());
                if (getterResults != null && !getterResults.isEmpty()) {
                    for (String gr : getterResults) {
                        if (gr.startsWith("ENUM_SET:")) {
                            for (String eVal : gr.substring(9).split(",")) {
                                String parsed = constantExtractor.parseEnumSetElement(eVal);
                                if (!polymorphicEvents.contains(parsed)) polymorphicEvents.add(parsed);
                            }
                        } else {
                            if (!polymorphicEvents.contains(gr)) polymorphicEvents.add(gr);
                        }
                    }
                    return TriggerPoint.builder()
                            .event(resolvedValue)
                            .className(tp.getClassName())
                            .methodName(tp.getMethodName())
                            .sourceFile(tp.getSourceFile())
                            .sourceModule(tp.getSourceModule())
                            .stateMachineId(tp.getStateMachineId())
                            .sourceState(tp.getSourceState())
                            .lineNumber(tp.getLineNumber())
                            .polymorphicEvents(polymorphicEvents)
                            .constraint(tp.getConstraint())
                            .stateTypeFqn(tp.getStateTypeFqn())
                            .eventTypeFqn(tp.getEventTypeFqn())
                            .external(tp.isExternal())
                            .ambiguous(isAmbiguous)
                            .build();
                }
                
                if (cic.getAnonymousClassDeclaration() != null) {
                    final List<String> polyEventsRef = polymorphicEvents;
                    for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
                        if (declObj instanceof MethodDeclaration mDecl && mDecl.getName().getIdentifier().equals(methodName) && mDecl.getBody() != null) {
                            mDecl.getBody().accept(new ASTVisitor() {
                                @Override
                                public boolean visit(ReturnStatement rs) {
                                    if (rs.getExpression() != null) {
                                        List<Expression> tracedReturns = variableTracer.traceVariableAll(rs.getExpression());
                                        for (Expression tracedRet : tracedReturns) {
                                            constantExtractor.extractConstantsFromExpression(tracedRet, polyEventsRef);
                                        }
                                    }
                                    return super.visit(rs);
                                }
                            });
                        }
                    }
                }
            } else if (mi.getExpression() instanceof MethodInvocation innerMi) {
                methodName = innerMi.getName().getIdentifier();
                if (innerMi.getExpression() instanceof SimpleName snInner) {
                    varName = snInner.getIdentifier();
                } else {
                    String exprStr = innerMi.getExpression() != null ? innerMi.getExpression().toString() : "";
                    if (!exprStr.contains("(")) {
                        varName = exprStr;
                    }
                }
            } else {
                String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "this";
                if (!exprStr.contains("(")) {
                    varName = exprStr;
                }
            }
            }
        } else if (exprNode instanceof ClassInstanceCreation cic) {
            declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
            sourceMethod = "inline-instantiation";
            extractEventConstantsFromConstructor(cic, polymorphicEvents);
            if (polymorphicEvents.isEmpty()) {
                polymorphicEvents.add(declaredType);
            }
        } else if (exprNode instanceof ConditionalExpression cond) {
            Expression selectedBranch = selectTernaryBranch(cond, initialBindings);
            List<Expression> branches = new ArrayList<>();
            if (selectedBranch != null) {
                branches.add(selectedBranch);
                resolvedValue = selectedBranch.toString();
                exprNode = selectedBranch;
            } else {
                branches.add(cond.getThenExpression());
                branches.add(cond.getElseExpression());
            }
            for (Expression branch : branches) {
                if (branch instanceof ConditionalExpression nestedCond) {
                    // traceVariableAll collapses literal "true"/"false" conditions to a single branch;
                    // extract recursively so chained if-else assignments keep all enum variants.
                    constantExtractor.extractConstantsFromExpression(nestedCond, polymorphicEvents);
                    continue;
                }
                List<Expression> tracedBranch = variableTracer.traceVariableAll(branch);
                for (Expression tb : tracedBranch) {
                    if (tb instanceof ClassInstanceCreation cic) {
                        polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
                    } else {
                        constantExtractor.extractConstantsFromExpression(tb, polymorphicEvents);
                    }
                }
            }
            sourceMethod = "inline-ternary";
            declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
        } else if (exprNode instanceof SwitchExpression swExpr) {
            constantExtractor.extractConstantsFromExpression(swExpr, polymorphicEvents);
            sourceMethod = "inline-switch";
            declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
        } else if (exprNode instanceof SimpleName sn) {
            varName = sn.getIdentifier();
            if (varName.equals(varName.toUpperCase())) {
                polymorphicEvents.add(varName);
                methodName = null; 
            } else {
                methodName = "VariableReference"; 
            }
        } else if (exprNode instanceof QualifiedName qn) {
            polymorphicEvents.add(qn.getFullyQualifiedName());
            methodName = null;
        } else if (exprNode instanceof FieldAccess fa) {
            polymorphicEvents.add(fa.toString());
            methodName = null;
        } else if (exprNode instanceof StringLiteral sl) {
            polymorphicEvents.add(sl.getLiteralValue());
            methodName = null;
        } else if (exprNode instanceof ParenthesizedExpression pe && 
                   pe.getExpression() instanceof CastExpression ce && 
                   ce.getExpression() instanceof ClassInstanceCreation cic) {
            declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
            sourceMethod = "inline-instantiation";
            extractEventConstantsFromConstructor(cic, polymorphicEvents);
            if (polymorphicEvents.isEmpty()) {
                polymorphicEvents.add(declaredType);
            }
        } else if (exprNode instanceof CastExpression ce && 
                   ce.getExpression() instanceof ClassInstanceCreation cic) {
            declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
            sourceMethod = "inline-instantiation";
            extractEventConstantsFromConstructor(cic, polymorphicEvents);
            if (polymorphicEvents.isEmpty()) {
                polymorphicEvents.add(declaredType);
            }
        }
        
        if (methodName != null) {
            if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) {
                String scopeForVar = resolveScopeMethodForVariable(varName, entryMethod, path);
                Expression localSetterExpr = variableTracer.traceLocalSetter(scopeForVar, varName, methodName);
                if (localSetterExpr != null) {
                    List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
                    for (Expression tracedSetter : tracedSetters) {
                        constantExtractor.extractConstantsFromExpression(tracedSetter, polymorphicEvents);
                        if (tracedSetter instanceof ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) {
                            resolvedValue = cic.toString() + "." + methodName + "()";
                        }
                    }
                    if (!polymorphicEvents.isEmpty()) {
                        return TriggerPoint.builder()
                            .event(resolvedValue)
                            .className(tp.getClassName())
                            .methodName(tp.getMethodName())
                            .sourceFile(tp.getSourceFile())
                            .sourceModule(tp.getSourceModule())
                            .stateMachineId(tp.getStateMachineId())
                            .sourceState(tp.getSourceState())
                            .lineNumber(tp.getLineNumber())
                            .polymorphicEvents(polymorphicEvents)
                            .constraint(tp.getConstraint())
                            .stateTypeFqn(tp.getStateTypeFqn())
                            .eventTypeFqn(tp.getEventTypeFqn())
                            .external(tp.isExternal())
                            .ambiguous(isAmbiguous)
                            .build();
                    }
                }
            }
            
            if (varName != null && declaredType == null) {
                if (!varName.isEmpty() && !"this".equals(varName) && !"super".equals(varName)) {
                    String entryClassName = entryMethod.substring(0, entryMethod.lastIndexOf('.'));
                    String entryMethodName = entryMethod.substring(entryMethod.lastIndexOf('.') + 1);
                    TypeDeclaration currentTd = context.getTypeDeclaration(entryClassName);
                    if (currentTd != null) {
                        MethodDeclaration currentMd = context.findMethodDeclaration(currentTd, entryMethodName, true);
                        if (currentMd != null) {
                            final String[] extractedFinalTraced = {null, null};
                            final String targetVar = varName;
                            final String mName = methodName;
                            final String[] finalDeclaredType = {null};
                            final String[] finalSourceMethod = {null};
                            currentMd.accept(new ASTVisitor() {
                                public boolean visit(VariableDeclarationStatement node) {
                                    for (Object fragObj : node.fragments()) {
                                        if (fragObj instanceof VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(targetVar)) {
                                            if (vdf.getInitializer() != null) {
                                                Expression valueNode = variableTracer.traceVariable(vdf.getInitializer());
                                                if (valueNode instanceof ClassInstanceCreation cic) {
                                                    if (cic.getAnonymousClassDeclaration() != null) {
                                                        extractedFinalTraced[0] = cic.toString();
                                                        extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
                                                    } else {
                                                        finalDeclaredType[0] = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
                                                        finalSourceMethod[0] = "inline-instantiation";
                                                    }
                                                } else {
                                                    extractedFinalTraced[0] = valueNode.toString();
                                                    extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
                                                }
                                            }
                                        }
                                    }
                                    return super.visit(node);
                                }
                            });
                            if (finalDeclaredType[0] != null) {
                                declaredType = finalDeclaredType[0];
                                sourceMethod = finalSourceMethod[0];
                            }
                            if (extractedFinalTraced[0] != null) {
                                resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
                                if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
                                    ASTNode newNode = parseExpressionString(resolvedValue);
                                    if (newNode instanceof Expression newExpr) {
                                        trackKeyedMapLookupFlags(newExpr, path, callGraph, entryMethod, keyedMapLookupOnInitializer, pathBoundMapKeyOnInitializer);
                                        addConstantsFromTracedExpression(newExpr, polymorphicEvents, path, callGraph, entryMethod);
                                    }
                                }
                            }
                        }
                    }
                }
                
                if (declaredType == null) {
                    for (String methodFqn : path) {
                        declaredType = variableTracer.getVariableDeclaredType(methodFqn, varName);
                        if (declaredType != null) {
                            TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
                            CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
                            // Defer CHA accessor union for virtual getType/getEvent — allocation gate runs next.
                            if (!shouldDeferVirtualAccessorChaFill(methodName, path, callGraph)) {
                                List<String> values = resolveAccessorConstants(declaredType, methodName, cu);
                                if (values != null && !values.isEmpty()) {
                                    polymorphicEvents.addAll(values);
                                }
                            }
                            if (polymorphicEvents.isEmpty() && variableTracer != null) {
                                String initializer = variableTracer.traceLocalVariable(methodFqn, varName);
                                if (initializer != null && !initializer.equals(varName)) {
                                    ASTNode mapNode = parseExpressionString(initializer);
                                    if (mapNode instanceof Expression initExpr) {
                                        trackKeyedMapLookupFlags(initExpr, path, callGraph, methodFqn, keyedMapLookupOnInitializer, pathBoundMapKeyOnInitializer);
                                        addConstantsFromTracedExpression(initExpr, polymorphicEvents, path, callGraph, methodFqn);
                                    }
                                }
                            }
                            sourceMethod = methodFqn;
                            break;
                        }
                    }
                    if (declaredType == null && varName.matches("^[A-Z].*")) {
                        TypeDeclaration staticTd = context.getTypeDeclaration(varName);
                        if (staticTd != null) {
                            declaredType = context.getFqn(staticTd);
                            sourceMethod = "static-call";
                        } else if (context.getEnumValues(varName) != null) {
                            declaredType = varName;
                            sourceMethod = "static-call";
                        }
                    }
                }
            }
            
            if (declaredType == null && methodName != null && !methodName.equals("VariableReference")) {
                for (String methodFqn : path) {
                    TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
                    if (currentTd != null && context.findMethodDeclaration(currentTd, methodName, true) != null) {
                        CompilationUnit cu = currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
                        if (!shouldDeferVirtualAccessorChaFill(methodName, path, callGraph)) {
                            List<String> values = resolveAccessorConstants(context.getFqn(currentTd), methodName, cu);
                            if (values != null && !values.isEmpty()) {
                                polymorphicEvents.addAll(values);
                                sourceMethod = methodFqn;
                                break;
                            }
                        } else {
                            sourceMethod = methodFqn;
                            break;
                        }
                    }
                }
            }

            if (debug) {
                log.debug("declaredType before getEnumValues: {} for exprNode: {} in {}", declaredType, exprNode, entryMethod);
            }
            if (declaredType != null) {
                List<String> enumValues = context.getEnumValues(declaredType);
                if (enumValues != null && !enumValues.isEmpty()) {
                    // Prefer allocation proof over any prior CHA/accessor fill (clear+replace on success).
                    if (tryResolveAllocationSensitiveAccessor(
                                    exprNode,
                                    methodName,
                                    path,
                                    callGraph,
                                    polymorphicEvents)) {
                        isAmbiguous = polymorphicEvents.size() > 1;
                    }
                    if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
                            && polymorphicEvents.isEmpty()
                            && exprNode instanceof MethodInvocation miArg
                            && entryMethod != null
                            && entryMethod.contains(".")) {
                        String entryClass = entryMethod.substring(0, entryMethod.lastIndexOf('.'));
                        List<String> methodReturns = constantExtractor.resolveMethodReturnConstant(
                                entryClass,
                                miArg.getName().getIdentifier(),
                                0,
                                new java.util.HashSet<>(),
                                null);
                        for (String methodReturn : methodReturns) {
                            if (methodReturn != null && !polymorphicEvents.contains(methodReturn)) {
                                polymorphicEvents.add(methodReturn);
                            }
                        }
                    }
                    if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
                            && polymorphicEvents.isEmpty()
                            && exprNode instanceof Expression expressionArg) {
                        addConstantsFromTracedExpression(expressionArg, polymorphicEvents, path, callGraph, entryMethod);
                    }
                    if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
                            && polymorphicEvents.isEmpty()
                            && tryRecoverAllocationSensitiveInto(path, callGraph, polymorphicEvents)) {
                        isAmbiguous = false;
                    }
                    if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
                            && !tp.isExternal()
                            && !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)
                            && !(keyedMapLookupOnInitializer[0] && pathBoundMapKeyOnInitializer[0])) {
                        String symbolic = "<SYMBOLIC: " + declaredType + ".*>";
                        if (!polymorphicEvents.contains(symbolic)) {
                            polymorphicEvents.add(symbolic);
                        }
                        isAmbiguous = true;
                    }
                } else {
                    // Allocation-sensitive virtual accessor: if the receiver is a known CIC
                    // (possibly forwarded through a param), resolve only that override's constant.
                    if (tryResolveAllocationSensitiveAccessor(
                            exprNode,
                            methodName,
                            path,
                            callGraph,
                            polymorphicEvents)) {
                        isAmbiguous = polymorphicEvents.size() > 1;
                    } else {
                    List<String> typesToInspect = new ArrayList<>();
                    if ("inline-instantiation".equals(sourceMethod)) {
                        typesToInspect.add(declaredType);
                    } else {
                        typesToInspect.add(declaredType);
                        typesToInspect.addAll(context.getImplementations(declaredType));
                    }
                    
                    for (String type : typesToInspect) {
                        TypeDeclaration baseTd = context.getTypeDeclaration(type);
                        CompilationUnit cuToUse = null;
                        if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
                            cuToUse = (CompilationUnit) baseTd.getRoot();
                        } else {
                            String firstPathMethod = path.get(0);
                            String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
                            TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
                            if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
                                cuToUse = (CompilationUnit) entryTd.getRoot();
                            }
                        }
                        List<String> constants = resolveAccessorConstants(type, methodName, cuToUse);
                        for (String constant : constants) {
                            if (!polymorphicEvents.contains(constant)) {
                                polymorphicEvents.add(constant);
                            }
                        }
                    }
                    if (polymorphicEvents.size() > 1) {
                        isAmbiguous = true;
                    }
                    if (polymorphicEvents.isEmpty()
                            && context.isAmbiguousSimpleName(declaredType)
                            && !tp.isExternal()
                            && !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)
                            && !(keyedMapLookupOnInitializer[0] && pathBoundMapKeyOnInitializer[0])) {
                        String symbolic = "<SYMBOLIC: " + declaredType + ".*>";
                        if (!polymorphicEvents.contains(symbolic)) {
                            polymorphicEvents.add(symbolic);
                        }
                        isAmbiguous = true;
                    }
                    }
                }
            }
        }

        boolean hasValidConstant = false;
        for (String ev : polymorphicEvents) {
            String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
            if (val.equals(val.toUpperCase()) && val.length() > 2) {
                hasValidConstant = true;
                break;
            }
        }
        
        if (!hasValidConstant && exprNode instanceof Expression expressionFallback) {
            addConstantsFromTracedExpression(expressionFallback, polymorphicEvents, path, callGraph, entryMethod);
            List<String> directConstants = new ArrayList<>();
            constantExtractor.extractConstantsFromExpression(expressionFallback, directConstants);
            for (String directConstant : directConstants) {
                if (directConstant != null && !directConstant.startsWith("<SYMBOLIC:")
                        && !polymorphicEvents.contains(directConstant)) {
                    polymorphicEvents.add(directConstant);
                }
            }
            if (expressionFallback instanceof MethodInvocation fallbackMi
                    && (fallbackMi.getExpression() == null || fallbackMi.getExpression() instanceof ThisExpression)) {
                TypeDeclaration enclosingType = findEnclosingType(fallbackMi);
                CompilationUnit fallbackCu = fallbackMi.getRoot() instanceof CompilationUnit cu ? cu : null;
                List<String> ownerCandidates = new ArrayList<>();
                if (enclosingType != null) {
                    ownerCandidates.add(context.getFqn(enclosingType));
                }
                // String-parsed expressions have no enclosing AST type — walk the call path instead.
                for (String methodFqn : path) {
                    if (methodFqn != null && methodFqn.contains(".")) {
                        String owner = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
                        if (!ownerCandidates.contains(owner)) {
                            ownerCandidates.add(owner);
                        }
                    }
                }
                for (String ownerFqn : ownerCandidates) {
                    TypeDeclaration ownerTd = context.getTypeDeclaration(ownerFqn);
                    if (ownerTd == null
                            || context.findMethodDeclaration(ownerTd, fallbackMi.getName().getIdentifier(), true) == null) {
                        continue;
                    }
                    if (enclosingType != null && ownerFqn.equals(context.getFqn(enclosingType))) {
                        List<String> switchConstants = resolveLocalSwitchMethodConstants(enclosingType, fallbackMi);
                        for (String switchConstant : switchConstants) {
                            if (switchConstant != null && !switchConstant.startsWith("<SYMBOLIC:")
                                    && !polymorphicEvents.contains(switchConstant)) {
                                polymorphicEvents.add(switchConstant);
                            }
                        }
                    }
                    List<String> methodReturns = constantExtractor.resolveMethodReturnConstant(
                            ownerFqn,
                            fallbackMi.getName().getIdentifier(),
                            0,
                            new HashSet<>(),
                            fallbackCu);
                    for (String methodReturn : methodReturns) {
                        if (methodReturn != null && !methodReturn.startsWith("<SYMBOLIC:")
                                && !polymorphicEvents.contains(methodReturn)) {
                            polymorphicEvents.add(methodReturn);
                        }
                    }
                    if (!polymorphicEvents.isEmpty()) {
                        break;
                    }
                }
            }
        }
        
        List<String> newPolyEvents = new ArrayList<>();
        for (String pe : polymorphicEvents) {
            List<String> resolved = constantExtractor.resolveClassConstantReturns(pe, context, null);
            if (resolved != null && !resolved.isEmpty()) {
                newPolyEvents.addAll(resolved);
            } else {
                newPolyEvents.add(pe);
            }
        }
        polymorphicEvents = newPolyEvents;

        String targetMethod = path.get(path.size() - 1);
        int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event, true);
        if (eventParamIndex < 0) {
            eventParamIndex = 0;
        }
        String expectedType = typeResolver.getParameterType(targetMethod, eventParamIndex);

        polymorphicEvents.removeIf(e -> shouldDropBarePolymorphicCandidate(e, expectedType, context));
        if (expectedType != null) {
            final String expType = expectedType;
            boolean isExpectedEnum = context.getEnumValues(expType) != null;
            polymorphicEvents.removeIf(e -> {
                if (e.contains(".")) {
                    String actualType = e.substring(0, e.lastIndexOf('.'));
                    boolean isActualEnum = context.getEnumValues(actualType) != null;
                    if (isExpectedEnum && isActualEnum) {
                        return !typeResolver.isTypeCompatible(actualType, expType);
                    }
                }
                return false;
            });
            qualifyBareEnumConstants(polymorphicEvents, expType);
        }
        
        List<String> unpacked = new ArrayList<>();
        for (String ev : polymorphicEvents) {
            if (ev != null) {
                if (ev.startsWith("ENUM_SET:")) {
                    for (String part : ev.substring(9).split(",")) {
                        String clean = constantExtractor.parseEnumSetElement(part);
                        if (clean != null && !unpacked.contains(clean)) {
                            unpacked.add(clean);
                        }
                    }
                } else if (ev.startsWith("<SYMBOLIC:") || ev.contains("SYMBOLIC:") || ev.endsWith(".*>")) {
                    if (!unpacked.contains(ev)) {
                        unpacked.add(ev);
                    }
                } else {
                    String clean = constantExtractor.parseEnumSetElement(ev);
                    if (clean != null && !unpacked.contains(clean)) {
                        unpacked.add(clean);
                    }
                }
            }
        }
        polymorphicEvents = unpacked;
        
        polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());

        polymorphicEvents.removeIf(pe -> pe != null && (pe.equals("*>") || pe.equals(".*>")));

        polymorphicEvents = narrowPolymorphicEventsWithPathBindings(polymorphicEvents, path, callGraph, tp);

        // sendEvent arg may already be <SYMBOLIC:Enum.*> / full enum widen while the dispatcher
        // frame still has valueOf(wrapper.getEvent().getType()) with a proven CIC — narrow here.
        // Skip only when poly already holds a unique concrete (SYMBOLIC size==1 must still recover).
        if (!hasUniqueConcretePolymorphic(polymorphicEvents)
                && tryRecoverAllocationSensitiveInto(path, callGraph, polymorphicEvents)) {
            isAmbiguous = false;
        }

        Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
        boolean hasConcretePolymorphic = polymorphicEvents.stream()
                .anyMatch(pe -> pe != null && !pe.startsWith("<SYMBOLIC:"));
        if (hasConcretePolymorphic
                && (isRuntimeEnumParameter(runtimeExpr) || tp.isExternal()
                        || DynamicTriggerExpressions.isDynamic(resolvedValue))) {
            polymorphicEvents.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
        }
        
        if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
            return TriggerPoint.builder()
                    .event(resolvedValue)
                    .className(tp.getClassName())
                    .methodName(tp.getMethodName())
                    .sourceFile(tp.getSourceFile())
                    .lineNumber(tp.getLineNumber())
                    .sourceModule(tp.getSourceModule())
                    .stateMachineId(tp.getStateMachineId())
                    .sourceState(tp.getSourceState())
                    .polymorphicEvents(polymorphicEvents)
                    .constraint(tp.getConstraint())
                    .stateTypeFqn(tp.getStateTypeFqn())
                    .eventTypeFqn(tp.getEventTypeFqn())
                    .external(tp.isExternal())
                    .ambiguous(isAmbiguous || polymorphicEvents.size() > 1)
                    .build();
        }

        if (polymorphicEvents.isEmpty()) {
            String[] extracted = extractMethodSuffix(resolvedValue, "", entryMethod);
            if (expressionAccessClassifier.isTraceableAccessorSuffix(extracted[1])) {
                String getterExpression = extracted[0] + extracted[1];
                List<String> getterEvents = evaluateGetterOnReceiver(
                        extracted[0], extracted[1], entryMethod, path, callGraph);
                if (getterEvents == null || getterEvents.isEmpty()) {
                    String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
                            getterExpression, entryMethod, constantResolver, context);
                    if (remapped != null) {
                        getterEvents = evaluateGetterOnReceiver(remapped, "", entryMethod, path, callGraph);
                    }
                }
                if (getterEvents != null && !getterEvents.isEmpty()) {
                    return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, getterEvents);
                }
            }
            if (exprNode instanceof ClassInstanceCreation cic) {
                polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
                return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
            }
            // valueOf(e.getType()) may have been collapsed to <SYMBOLIC:Enum.*> on the sendEvent
            // edge — recover via event-identity resolution on the dispatcher frame.
            List<String> recovered = eventIdentityResolver != null
                    ? eventIdentityResolver.recoverFromPath(path, callGraph != null ? callGraph : this.graph)
                    : List.of();
            if (recovered.size() == 1) {
                return buildTriggerPointWithPolymorphicEvents(
                        tp,
                        "valueOf(alloc.getType())",
                        recovered);
            }
        }
        
        return tp;
        } finally {
            context.clearAnalysisCallPath();
        }
    }

    protected MethodDeclaration findEnclosingMethod(ASTNode node) {
        return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
    }

    protected TypeDeclaration findEnclosingType(ASTNode node) {
        return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
    }

    protected List<String> resolveArguments(List<?> astArguments) {
        List<String> args = new ArrayList<>();
        for (Object argObj : astArguments) {
            args.add(resolveArgument((Expression) argObj));
        }
        return args;
    }

    protected String resolveArgument(Expression expr) {
        if (expr == null) {
            return "null";
        }
        if (expr instanceof LambdaExpression le) {
            ASTNode body = le.getBody();
            if (body instanceof Expression bodyExpr) {
                expr = bodyExpr;
            } else if (body instanceof Block block) {
                for (Object stmtObj : block.statements()) {
                    if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
                        expr = rs.getExpression();
                        break;
                    }
                }
            }
            if (expr instanceof LambdaExpression) {
                String lambdaText = expr.toString();
                String val = constantResolver.resolve(expr, context);
                return val != null ? val : lambdaText;
            }
        }

        if (expr instanceof ExpressionMethodReference emr) {
            TypeDeclaration td = findEnclosingType(emr);
            if (td != null) {
                MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
                if (md != null && md.getBody() != null) {
                    for (Object stmtObj : md.getBody().statements()) {
                        if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
                            expr = rs.getExpression();
                            break;
                        }
                    }
                }
            }
        }

        if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
            String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
            TypeDeclaration td = context.getTypeDeclaration(typeName);
            boolean shouldUnwrap = true;
            if (td != null) {
                for (MethodDeclaration md : td.getMethods()) {
                    if (!md.isConstructor()) {
                        shouldUnwrap = false;
                        break;
                    }
                }
            }
            if (shouldUnwrap) {
                Expression firstArg = (Expression) cic.arguments().get(0);
                String resolved = constantResolver.resolve(firstArg, context);
                if (resolved != null) {
                    expr = firstArg;
                }
            }
        }

        String val = constantResolver.resolve(expr, context);
        if (val == null) {
            val = expr.toString(); 
        }

        return postProcessResolvedArgument(expr, val);
    }

    protected String postProcessResolvedArgument(Expression originalExpr, String resolvedValue) {
        // Preserve valueOf(variable) so trigger tracing can bind the variable per call-path
        // instead of widening to all enum constants at graph-build time.
        if (originalExpr instanceof MethodInvocation valueOfMi
                && "valueOf".equals(valueOfMi.getName().getIdentifier())
                && valueOfMi.arguments().size() == 1
                && valueOfMi.arguments().get(0) instanceof SimpleName) {
            return originalExpr.toString();
        }

        // Preserve routing helper calls for per-endpoint literal forwarding.
        if (originalExpr instanceof MethodInvocation helperMi
                && helperMi.arguments().isEmpty()
                && isLikelyRoutingHelperReceiver(helperMi.getExpression())
                && resolvedValue != null
                && resolvedValue.matches("^[A-Z_][A-Z0-9_]*$")) {
            return originalExpr.toString();
        }

        // Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
        // where we can substitute the actual receiver from the call edge.
        boolean isThisOrSuperGetter = (originalExpr instanceof MethodInvocation mi
                && (mi.getExpression() instanceof ThisExpression || mi.getExpression() instanceof SuperMethodInvocation)
                && mi.arguments().isEmpty()) || (originalExpr instanceof SuperMethodInvocation smi && smi.arguments().isEmpty());

        if (isThisOrSuperGetter) {
            return originalExpr.toString();
        }

        // Narrow resolution for "localVar.getX()" patterns where localVar is initialized
        // from a ClassInstanceCreation (directly or via a factory method).
        if (originalExpr instanceof MethodInvocation getterMi
                && getterMi.getExpression() instanceof SimpleName instanceSn
                && getterMi.arguments().isEmpty()) {
            String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
            if (narrowed != null) {
                return narrowed;
            }
        }

        return resolvedValue;
    }

    private boolean isLikelyRoutingHelperReceiver(Expression receiver) {
        if (receiver instanceof SimpleName simpleName) {
            String identifier = simpleName.getIdentifier();
            return !identifier.isEmpty()
                    && (Character.isLowerCase(identifier.charAt(0))
                    || Character.isUpperCase(identifier.charAt(0)));
        }
        if (receiver instanceof QualifiedName qualifiedName) {
            String identifier = qualifiedName.getName().getIdentifier();
            return !identifier.isEmpty() && Character.isUpperCase(identifier.charAt(0));
        }
        if (receiver instanceof TypeLiteral typeLiteral) {
            String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils
                    .extractSimpleTypeName(typeLiteral.getType());
            return typeName != null && !typeName.isEmpty() && Character.isUpperCase(typeName.charAt(0));
        }
        return false;
    }

    protected String getReceiverString(Expression receiver) {
        if (receiver == null) {
            return null;
        }
        if (receiver instanceof ThisExpression) {
            return "this";
        }
        if (receiver instanceof SuperMethodInvocation) {
            return "super";
        }
        if (receiver instanceof SimpleName sn) {
            return sn.getIdentifier();
        }
        return receiver.toString();
    }

    /**
     * Best-effort type of the call receiver for call-graph edges. Implicit {@code this} uses the
     * enclosing type; simple names use field/param/local resolution.
     */
    protected String resolveReceiverTypeFqn(Expression receiver, TypeDeclaration enclosingType) {
        if (receiver == null || receiver instanceof ThisExpression) {
            return enclosingType != null ? context.getFqn(enclosingType) : null;
        }
        if (receiver instanceof SimpleName sn) {
            return resolveReceiverTypeFallback(sn);
        }
        if (receiver instanceof FieldAccess fa) {
            return resolveReceiverTypeFallback(fa.getName());
        }
        if (receiver instanceof MethodInvocation mi) {
            return resolveMethodInvocationReturnType(mi);
        }
        ITypeBinding binding = receiver.resolveTypeBinding();
        if (binding != null) {
            return binding.getErasure().getQualifiedName();
        }
        return null;
    }

    /**
     * When a unique Spring bean can be proven for the receiver, return that concrete type FQN.
     * Default: none (Heuristic engine). {@link JdtCallGraphEngine} overrides via injection analysis.
     */
    protected String resolveInjectedReceiverTypeFqn(Expression receiver) {
        return null;
    }

    /**
     * When the resolved receiver type is a scanned FQN but {@code calledMethod} was qualified with the
     * caller's package (same simple name), re-home the target onto the receiver type.
     */
    protected String qualifyCalledMethodWithReceiverType(String calledMethod, String receiverTypeFqn) {
        if (calledMethod == null || receiverTypeFqn == null
                || !calledMethod.contains(".") || !receiverTypeFqn.contains(".")) {
            return calledMethod;
        }
        if (context.getTypeDeclaration(receiverTypeFqn) == null) {
            return calledMethod;
        }
        int methodDot = calledMethod.lastIndexOf('.');
        String methodName = calledMethod.substring(methodDot + 1);
        String calledClass = calledMethod.substring(0, methodDot);
        if (calledClass.equals(receiverTypeFqn)) {
            return calledMethod;
        }
        String calledSimple = calledClass.contains(".")
                ? calledClass.substring(calledClass.lastIndexOf('.') + 1)
                : calledClass;
        String receiverSimple = receiverTypeFqn.substring(receiverTypeFqn.lastIndexOf('.') + 1);
        if (calledSimple.equals(receiverSimple)) {
            return receiverTypeFqn + "." + methodName;
        }
        return calledMethod;
    }

    /**
     * For CHA-expanded targets {@code Impl.method}, prefer the concrete impl class as the edge's
     * receiver type so pathfinding can prune sibling overrides inside that collaborator hierarchy.
     */
    protected String refineReceiverTypeForTarget(String declaredReceiverTypeFqn, String calledMethodFqn) {
        if (calledMethodFqn == null || !calledMethodFqn.contains(".")) {
            return declaredReceiverTypeFqn;
        }
        String targetClass = calledMethodFqn.substring(0, calledMethodFqn.lastIndexOf('.'));
        if (targetClass.contains("<")) {
            targetClass = targetClass.substring(0, targetClass.indexOf('<'));
        }
        if (!context.isConcreteClassType(targetClass)) {
            return declaredReceiverTypeFqn;
        }
        if (declaredReceiverTypeFqn == null
                || declaredReceiverTypeFqn.isBlank()
                || context.isSubtypeOrSame(targetClass, declaredReceiverTypeFqn)
                || context.areSameTypeOrUnambiguousSimpleMatch(targetClass, declaredReceiverTypeFqn)) {
            return targetClass;
        }
        return declaredReceiverTypeFqn;
    }

    protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
        int lastDot = callerFqn.lastIndexOf('.');
        if (lastDot < 0) {
            return null;
        }
        String callerClass = callerFqn.substring(0, lastDot);
        String callerMethod = callerFqn.substring(lastDot + 1);

        int targetLastDot = targetMethodFqn.lastIndexOf('.');
        if (targetLastDot < 0) {
            return null;
        }
        String targetMethodName = targetMethodFqn.substring(targetLastDot + 1);

        TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
        if (callerTd == null) {
            return null;
        }

        MethodDeclaration callerMd = context.findMethodDeclaration(callerTd, callerMethod, false);
        if (callerMd == null || callerMd.getBody() == null) {
            return null;
        }

        final String[] foundReceiver = {null};
        callerMd.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if (foundReceiver[0] != null) {
                    return false;
                }
                String called = resolveCalledMethod(node);
                if (called != null && (called.equals(targetMethodFqn) || called.endsWith("." + targetMethodName))) {
                    foundReceiver[0] = getReceiverString(node.getExpression());
                    return false;
                }
                return super.visit(node);
            }
        });
        return foundReceiver[0];
    }

    protected String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
        String varName = receiverNameNode.getIdentifier();

        CompilationUnit cu = receiverNameNode.getRoot() instanceof CompilationUnit rootCu ? rootCu : null;
        TypeDeclaration staticTd = context.getTypeDeclaration(varName, cu);
        if (staticTd != null) {
            return context.getFqn(staticTd);
        }
        TypeDeclaration fallbackStaticTd = context.getTypeDeclaration(varName);
        if (fallbackStaticTd != null) {
            return context.getFqn(fallbackStaticTd);
        }

        MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
        if (enclosingMethod != null) {
            for (Object paramObj : enclosingMethod.parameters()) {
                SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
                if (svd.getName().getIdentifier().equals(varName)) {
                    return resolveTypeToFqn(svd.getType(), receiverNameNode);
                }
            }
            if (enclosingMethod.getBody() != null) {
                Type[] foundType = new Type[1];
                enclosingMethod.getBody().accept(new ASTVisitor() {
                    @Override
                    public boolean visit(VariableDeclarationStatement node) {
                        for (Object fragObj : node.fragments()) {
                            VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                            if (frag.getName().getIdentifier().equals(varName)) {
                                foundType[0] = node.getType();
                            }
                        }
                        return super.visit(node);
                    }
                });
                if (foundType[0] != null) {
                    return resolveTypeToFqn(foundType[0], receiverNameNode);
                }
            }
        }

        TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
        final String[] resolvedType = {null};
        context.forEachSuperclass(enclosingType, currentType -> {
            if (resolvedType[0] != null) {
                return;
            }
            for (FieldDeclaration field : currentType.getFields()) {
                for (Object fragObj : field.fragments()) {
                    VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                    if (frag.getName().getIdentifier().equals(varName)) {
                        resolvedType[0] = resolveTypeToFqn(field.getType(), receiverNameNode);
                    }
                }
            }
        });
        return resolvedType[0];
    }

    protected String resolveTypeToFqn(Type type, ASTNode contextNode) {
        return typeResolver.resolveTypeToFqn(type, contextNode);
    }

    protected List<String> resolveAccessorConstants(String ownerFqn, String methodName, CompilationUnit contextCu) {
        if (ownerFqn == null || methodName == null) {
            return List.of();
        }
        TypeDeclaration ownerType = contextCu != null
                ? context.getTypeDeclaration(ownerFqn, contextCu)
                : context.getTypeDeclaration(ownerFqn);
        if (ownerType == null) {
            ownerType = context.getTypeDeclaration(ownerFqn);
        }
        return accessorResolver.resolve(
                ownerFqn,
                methodName,
                new AccessorResolver.ReceiverContext(ownerType, null, contextCu, false, null),
                ResolutionBudget.defaults());
    }

    /**
     * When a virtual accessor ({@code getType}/{@code getA}/…) has a receiver proven to be a
     * concrete {@code new T()} (or concrete declared type), resolve only that override's
     * compile-time constant instead of unioning all interface implementors.
     *
     * @return true when a unique constant was proven and written into {@code polymorphicEvents}
     */
    private boolean tryResolveAllocationSensitiveAccessor(
            Object exprNode,
            String methodName,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            List<String> polymorphicEvents) {
        if (!(exprNode instanceof Expression expression) || eventIdentityResolver == null) {
            return false;
        }
        Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
        List<String> unique = eventIdentityResolver.resolveUnique(expression, path, graphForFacts);
        if (unique.size() != 1) {
            // Prefer peeling a nested accessor when methodName hints at get*
            if (methodName != null && EventIdentityResolver.isVirtualAccessorName(methodName)) {
                MethodInvocation accessorCall = findVirtualAccessorInvocation(expression, methodName);
                if (accessorCall != null && accessorCall != expression) {
                    unique = eventIdentityResolver.resolveUnique(accessorCall, path, graphForFacts);
                }
            }
        }
        if (unique.size() != 1) {
            return false;
        }
        polymorphicEvents.clear();
        polymorphicEvents.add(unique.get(0));
        return true;
    }

    /**
     * Prefer the named accessor on the expression itself; for {@code Enum.valueOf(e.getType())}
     * dig into the string argument.
     */
    private static MethodInvocation findVirtualAccessorInvocation(Expression expression, String preferredName) {
        Expression peeled = expression;
        while (peeled instanceof ParenthesizedExpression pe) {
            peeled = pe.getExpression();
        }
        if (peeled instanceof MethodInvocation mi) {
            String name = mi.getName().getIdentifier();
            if (preferredName != null && preferredName.equals(name)
                    && EventIdentityResolver.isVirtualAccessorName(name)) {
                return mi;
            }
            if ("valueOf".equals(name) && mi.arguments().size() == 1
                    && mi.arguments().get(0) instanceof Expression arg) {
                MethodInvocation nested = findVirtualAccessorInvocation(arg, null);
                if (nested != null) {
                    return nested;
                }
            }
            if (preferredName == null && EventIdentityResolver.isVirtualAccessorName(name)) {
                return mi;
            }
            if (EventIdentityResolver.isVirtualAccessorName(name)) {
                return mi;
            }
        }
        return null;
    }

    private static boolean isVirtualAccessorName(String name) {
        return EventIdentityResolver.isVirtualAccessorName(name);
    }

    private static boolean isEventIdentityAccessorName(String name) {
        return EventIdentityResolver.isEventIdentityAccessorName(name);
    }

    /**
     * Skip early CHA {@code resolveAccessorConstants} for virtual event accessors when a call path
     * exists — allocation-sensitive resolve runs next and must not start from a polluted multi-set.
     */
    private boolean shouldDeferVirtualAccessorChaFill(
            String methodName,
            List<String> path,
            Map<String, List<CallEdge>> callGraph) {
        if (!isVirtualAccessorName(methodName)) {
            return false;
        }
        if (path == null || path.size() < 2) {
            return false;
        }
        return callGraph != null || this.graph != null;
    }

    /**
     * Path-scoped recovery into {@code polymorphicEvents} when sendEvent's argument lost the
     * {@code valueOf(receiver.getType())} AST (collapsed to symbolic / enum widen).
     *
     * @return true when a unique allocation-sensitive constant was proven
     */
    private boolean tryRecoverAllocationSensitiveInto(
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            List<String> polymorphicEvents) {
        if (eventIdentityResolver == null) {
            return false;
        }
        Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
        List<String> recovered = eventIdentityResolver.recoverFromPath(path, graphForFacts);
        if (recovered.size() != 1) {
            return false;
        }
        polymorphicEvents.clear();
        polymorphicEvents.add(recovered.get(0));
        return true;
    }

    private String tryNarrowGetterViaLocalVarChain(MethodInvocation getterCall, SimpleName instanceSn) {
        MethodDeclaration enclosing = findEnclosingMethod(getterCall);
        if (enclosing == null || enclosing.getBody() == null) {
            return null;
        }

        String varName = instanceSn.getIdentifier();
        String getterName = getterCall.getName().getIdentifier();

        String[] varTypeName = {null};
        Expression[] initExpr = {null};
        enclosing.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(VariableDeclarationStatement vds) {
                for (Object fragObj : vds.fragments()) {
                    VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                    if (frag.getName().getIdentifier().equals(varName)) {
                        varTypeName[0] = vds.getType().toString();
                        initExpr[0] = frag.getInitializer();
                    }
                }
                return super.visit(vds);
            }
        });
        if (varTypeName[0] == null) {
            return null;
        }

        ClassInstanceCreation cic = null;
        String receiverType = null;
        if (initExpr[0] instanceof ClassInstanceCreation directCic) {
            cic = directCic;
        } else if (initExpr[0] instanceof MethodInvocation factoryMi
                && (factoryMi.getExpression() instanceof SimpleName || factoryMi.getExpression() instanceof QualifiedName)) {
            if (factoryMi.getExpression() instanceof SimpleName receiverSn) {
                receiverType = resolveReceiverTypeFallback(receiverSn);
            } else if (factoryMi.getExpression() instanceof QualifiedName qn) {
                receiverType = qn.getFullyQualifiedName();
            }
            if (receiverType != null) {
                cic = findReturnedCIC(receiverType, factoryMi.getName().getIdentifier(), context);
            }
        }
        if (cic == null) {
            if (initExpr[0] instanceof MethodInvocation initMi) {
                String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
                Expression currentMi = initMi;
                while (currentMi instanceof MethodInvocation chainMi) {
                    String mName = chainMi.getName().getIdentifier();
                    if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
                        if (!chainMi.arguments().isEmpty()) {
                            return chainMi.arguments().get(0).toString();
                        }
                    }
                    currentMi = chainMi.getExpression();
                }
            }
            return null;
        }

        CompilationUnit cu = (CompilationUnit) getterCall.getRoot();
        TypeDeclaration varTypeTd = context.getTypeDeclaration(varTypeName[0], cu);
        if (varTypeTd == null) {
            varTypeTd = context.getTypeDeclaration(varTypeName[0]);
        }
        if (varTypeTd == null) {
            return null;
        }

        Map<String, String> initialLocals = new HashMap<>();
        if (initExpr[0] instanceof MethodInvocation factoryMi) {
            MethodDeclaration md = context.findMethodDeclaration(
                    context.getTypeDeclaration(receiverType != null ? receiverType : varTypeName[0]),
                    factoryMi.getName().getIdentifier(),
                    true);
            if (md != null) {
                for (int i = 0; i < md.parameters().size() && i < factoryMi.arguments().size(); i++) {
                    SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
                    Expression arg = (Expression) factoryMi.arguments().get(i);
                    String resolvedVal = constantResolver.resolve(arg, context);
                    if (resolvedVal != null) {
                        initialLocals.put(param.getName().getIdentifier(), resolvedVal);
                    }
                }
            }
        }

        Map<String, String> fieldValues = constructorAnalyzer.buildFieldValuesFromCIC(
                varTypeTd, cic, context, this::evaluateMethodCallInConstructor, initialLocals);
        if (fieldValues.isEmpty()) {
            return null;
        }

        trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);

        List<String> resolved = accessorResolver.resolve(
                context.getFqn(varTypeTd),
                getterName,
                new AccessorResolver.ReceiverContext(varTypeTd, cic, cu, false, fieldValues),
                ResolutionBudget.defaults());
        if (!resolved.isEmpty()) {
            return resolved.get(0);
        }
        return null;
    }

    private void trackSetterCallsBetween(
            Block methodBody,
            String varName,
            MethodInvocation getterCall,
            Map<String, String> fieldValues,
            CodebaseContext context) {
        List<ASTNode> statements = new ArrayList<>();
        for (Object stmtObj : methodBody.statements()) {
            statements.add((ASTNode) stmtObj);
        }

        int varDeclIndex = -1;
        int getterCallIndex = -1;
        for (int i = 0; i < statements.size(); i++) {
            ASTNode stmt = statements.get(i);
            if (varDeclIndex == -1 && stmt instanceof VariableDeclarationStatement vds) {
                for (Object fragObj : vds.fragments()) {
                    if (fragObj instanceof VariableDeclarationFragment frag
                            && frag.getName().getIdentifier().equals(varName)) {
                        varDeclIndex = i;
                        break;
                    }
                }
            }
            if (getterCallIndex == -1 && stmt.toString().contains(getterCall.toString())) {
                getterCallIndex = i;
                break;
            }
        }

        if (varDeclIndex < 0 || getterCallIndex <= varDeclIndex) {
            return;
        }

        for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
            ASTNode stmt = statements.get(i);
            if (stmt instanceof ExpressionStatement es) {
                Expression expr = es.getExpression();
                if (expr instanceof MethodInvocation mi) {
                    if (mi.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
                        String methodName = mi.getName().getIdentifier();
                        if (methodName.startsWith("set") && methodName.length() > 3 && !mi.arguments().isEmpty()) {
                            String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                            String val = constantResolver.resolve((Expression) mi.arguments().get(0), context);
                            if (val != null) {
                                fieldValues.put(fieldName, val);
                                fieldValues.put("this." + fieldName, val);
                            }
                        }
                    }
                } else if (expr instanceof Assignment assignment) {
                    if (assignment.getLeftHandSide() instanceof FieldAccess fa) {
                        if (fa.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
                            String val = constantResolver.resolve(assignment.getRightHandSide(), context);
                            if (val != null) {
                                fieldValues.put(fa.getName().getIdentifier(), val);
                                fieldValues.put("this." + fa.getName().getIdentifier(), val);
                            }
                        }
                    }
                }
            }
        }
    }

    protected String resolveMethodInType(TypeDeclaration td, String methodName) {
        MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
        if (md != null) {
            TypeDeclaration declaringTd = findEnclosingType(md);
            return context.getFqn(declaringTd) + "." + methodName;
        }
        CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
        TypeDeclaration targetTd = context.resolveStaticImport(methodName, cu);
        if (targetTd != null) {
            return context.getFqn(targetTd) + "." + methodName;
        }
        return null;
    }

    protected String resolveMethodInTypeHierarchy(TypeDeclaration td, String methodName) {
        String resolved = resolveMethodInType(td, methodName);
        if (resolved != null) {
            return resolved;
        }
        String superFqn = context.getSuperclassFqn(td);
        TypeDeclaration superTd = superFqn != null ? context.getTypeDeclaration(superFqn) : null;
        if (superTd == null) {
            return null;
        }
        final String[] found = {null};
        context.forEachSuperclass(superTd, current -> {
            if (found[0] != null) {
                return;
            }
            found[0] = resolveMethodInType(current, methodName);
        });
        return found[0];
    }

    protected Expression traceVariable(Expression expr) {
        return variableTracer.traceVariable(expr);
    }

    protected Map<String, String> buildFieldValuesFromCIC(
            TypeDeclaration td,
            ClassInstanceCreation cic,
            CodebaseContext context,
            RhsResolver rhsResolver) {
        return constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, (rhs, paramValues, typeDecl) -> rhsResolver.resolve(rhs, paramValues, typeDecl));
    }

    protected Map<String, String> buildFieldValuesFromCIC(
            TypeDeclaration td,
            ClassInstanceCreation cic,
            CodebaseContext context) {
        return constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context);
    }

    protected String evaluateMethodCallInConstructor(
            Expression rhs,
            Map<String, String> paramValues,
            TypeDeclaration td) {
        return constructorAnalyzer.evaluateMethodCallInConstructor(rhs, paramValues, td);
    }

    protected ClassInstanceCreation findReturnedCIC(
            String className, String methodName, CodebaseContext context) {
        return constructorAnalyzer.findReturnedCIC(className, methodName, context);
    }

    protected record GetterChainReceiver(String typeFqn, ClassInstanceCreation cic) {}

    /**
     * Resolves a nested getter prefix such as {@code wrapper.getEvent()} to the declared return type
     * and, when possible, a {@code new ...()} instance from the getter body.
     */
    private TriggerPoint buildTriggerPointWithPolymorphicEvents(TriggerPoint tp, String resolvedValue, List<String> polymorphicEvents) {
        boolean ambiguous = tp.isAmbiguous()
                || (polymorphicEvents != null && polymorphicEvents.size() > 1);
        return TriggerPoint.builder()
                .event(resolvedValue)
                .className(tp.getClassName())
                .methodName(tp.getMethodName())
                .sourceFile(tp.getSourceFile())
                .sourceModule(tp.getSourceModule())
                .stateMachineId(tp.getStateMachineId())
                .sourceState(tp.getSourceState())
                .lineNumber(tp.getLineNumber())
                .polymorphicEvents(polymorphicEvents)
                .constraint(tp.getConstraint())
                .stateTypeFqn(tp.getStateTypeFqn())
                .eventTypeFqn(tp.getEventTypeFqn())
                .external(tp.isExternal())
                .ambiguous(ambiguous)
                .build();
    }

    private List<String> resolveSetterEventsFromSuffix(
            String currentParamName,
            String methodSuffix,
            String entryMethod,
            List<String> path) {
        String localMethodName = methodSuffix.substring(1).replace("()", "");
        String scopeForVar = resolveScopeMethodForVariable(currentParamName, entryMethod, path);
        Expression localSetterExpr = variableTracer.traceLocalSetter(scopeForVar, currentParamName, localMethodName);
        if (localSetterExpr == null) {
            return List.of();
        }
        List<String> setterEvents = new ArrayList<>();
        List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
        for (Expression tracedSetter : tracedSetters) {
            constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
        }
        return setterEvents;
    }

    protected GetterChainReceiver resolveGetterChainToReceiver(Expression expr, String entryMethod) {
        if (expr instanceof SimpleName sn) {
            String type = variableTracer.getVariableDeclaredType(entryMethod, sn.getIdentifier());
            if (type == null) {
                return null;
            }
            Expression init = findVariableInitializer(entryMethod, sn.getIdentifier());
            ClassInstanceCreation cic = init instanceof ClassInstanceCreation c ? c : null;
            return new GetterChainReceiver(type, cic);
        }
        if (expr instanceof ThisExpression) {
            int lastDot = entryMethod.lastIndexOf('.');
            if (lastDot <= 0) {
                return null;
            }
            String className = entryMethod.substring(0, lastDot);
            return new GetterChainReceiver(className, null);
        }
        if (expr instanceof ClassInstanceCreation cic) {
            return new GetterChainReceiver(
                    click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()),
                    cic);
        }
        if (!(expr instanceof MethodInvocation mi)) {
            return null;
        }

        GetterChainReceiver base = resolveGetterChainToReceiver(mi.getExpression(), entryMethod);
        if (base == null) {
            return null;
        }

        String getterName = mi.getName().getIdentifier();
        String simpleType = simplifyTypeName(base.typeFqn());
        String ownerFqn = resolveOwnerFqn(simpleType, base.typeFqn());

        AccessorResolver.GetterChainStep indexedStep =
                accessorResolver.resolveGetterChainStep(ownerFqn, getterName, ResolutionBudget.defaults());
        if (indexedStep != null) {
            return new GetterChainReceiver(indexedStep.typeFqn(), indexedStep.cic());
        }

        ClassInstanceCreation nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
        if (nextCic != null) {
            return new GetterChainReceiver(
                    click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType()),
                    nextCic);
        }

        TypeDeclaration td = context.getTypeDeclaration(simpleType);
        if (td == null) {
            td = context.getTypeDeclaration(base.typeFqn());
        }
        if (td != null) {
            MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
            if (getter != null && getter.getReturnType2() != null) {
                return new GetterChainReceiver(getter.getReturnType2().toString(), null);
            }
        }
        return null;
    }

    private String resolveOwnerFqn(String simpleType, String typeFqn) {
        TypeDeclaration td = context.getTypeDeclaration(simpleType);
        if (td == null && typeFqn != null) {
            td = context.getTypeDeclaration(typeFqn);
        }
        return td != null ? context.getFqn(td) : typeFqn;
    }

    private static String simplifyTypeName(String typeFqn) {
        if (typeFqn == null) {
            return null;
        }
        String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
        if (simple.contains("<")) {
            simple = simple.substring(0, simple.indexOf('<'));
        }
        return simple;
    }

    private String resolveScopeMethodForVariable(String varName, String entryMethod, List<String> path) {
        if (varName != null && path != null) {
            for (String methodFqn : path) {
                if (variableTracer.getVariableDeclaredType(methodFqn, varName) != null) {
                    return methodFqn;
                }
            }
        }
        return entryMethod;
    }

    private String resolveClassForSuperCall(String entryMethod, List<String> path) {
        if (path != null) {
            for (String methodFqn : path) {
                if (methodFqn == null) {
                    continue;
                }
                int lastDot = methodFqn.lastIndexOf('.');
                if (lastDot <= 0) {
                    continue;
                }
                String className = methodFqn.substring(0, lastDot);
                TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
                if (typeDeclaration == null) {
                    continue;
                }
                String superFqn = context.getSuperclassFqn(typeDeclaration);
                if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
                    return className;
                }
            }
        }
        int lastDot = entryMethod.lastIndexOf('.');
        return lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
    }

    protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
        String expressionText = click.kamil.springstatemachineexporter.ast.common.AstUtils.combineExpressionText(
                resolvedValue, methodSuffix);
        List<String> searchMethods = buildGetterSearchMethods(resolvedValue, entryMethod, path);

        // Prefer allocation-/identity-sensitive proof before CHA accessor union.
        List<String> viaEir = resolveGetterConstantsViaEventIdentity(
                expressionText, searchMethods, path, callGraph);
        if (viaEir.size() == 1) {
            return viaEir;
        }

        List<String> constants = resolveGetterConstantsViaDataflow(expressionText, searchMethods, path, callGraph, entryMethod);
        if (!constants.isEmpty()) {
            return constants;
        }

        for (String scopeMethod : searchMethods) {
            String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
                    expressionText, scopeMethod, constantResolver, context);
            if (remapped != null && !remapped.equals(expressionText)) {
                viaEir = resolveGetterConstantsViaEventIdentity(remapped, searchMethods, path, callGraph);
                if (viaEir.size() == 1) {
                    return viaEir;
                }
                constants = resolveGetterConstantsViaDataflow(remapped, searchMethods, path, callGraph, entryMethod);
                if (!constants.isEmpty()) {
                    return constants;
                }
            }
        }
        return resolveGetterConstantsViaAccessorPipeline(resolvedValue, methodSuffix, entryMethod, path);
    }

    private List<String> resolveGetterConstantsViaEventIdentity(
            String expressionText,
            List<String> searchMethods,
            List<String> path,
            Map<String, List<CallEdge>> callGraph) {
        if (eventIdentityResolver == null || expressionText == null || expressionText.isBlank()) {
            return List.of();
        }
        Expression anchored = click.kamil.springstatemachineexporter.ast.common.AstUtils.findExpressionInMethods(
                searchMethods, expressionText, context);
        if (anchored == null) {
            return List.of();
        }
        Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
        return eventIdentityResolver.resolveUnique(anchored, path, graphForFacts);
    }

    private List<String> resolveGetterConstantsViaDataflow(
            String expressionText,
            List<String> searchMethods,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            String entryMethod) {
        Expression anchored = click.kamil.springstatemachineexporter.ast.common.AstUtils.findExpressionInMethods(
                searchMethods, expressionText, context);
        if (anchored != null) {
            List<String> constants = new ArrayList<>();
            addConstantsFromTracedExpression(anchored, constants, path, callGraph, entryMethod);
            if (!constants.isEmpty()) {
                return constants.stream().distinct().toList();
            }
        }
        return variableTracer.resolveConstantsFromSourceText(expressionText, searchMethods, path);
    }

    private List<String> resolveGetterConstantsViaAccessorPipeline(String resolvedValue, String methodSuffix, String entryMethod, List<String> path) {
        ASTNode exprNode = parseExpressionString(resolvedValue);
        while (exprNode instanceof ParenthesizedExpression pe) {
            exprNode = pe.getExpression();
        }

        String scopeMethod = entryMethod;
        Expression rootExpr = exprNode instanceof Expression e ? e : null;
        if (rootExpr != null) {
            while (rootExpr instanceof MethodInvocation mi) {
                rootExpr = mi.getExpression();
            }
            if (rootExpr instanceof SimpleName sn) {
                scopeMethod = resolveScopeMethodForVariable(sn.getIdentifier(), entryMethod, path);
            }
        }

        if (exprNode instanceof SuperMethodInvocation smi) {
            String getterName = smi.getName().getIdentifier();
            String className = resolveClassForSuperCall(entryMethod, path);
            TypeDeclaration currentTd = context.getTypeDeclaration(className);
            if (currentTd != null) {
                String receiverType = context.getSuperclassFqn(currentTd);
                if (receiverType != null) {
                    CompilationUnit contextCu = currentTd.getRoot() instanceof CompilationUnit cu ? cu : null;
                    List<String> resolved = accessorResolver.resolve(
                            receiverType,
                            getterName,
                            new AccessorResolver.ReceiverContext(
                                    context.getTypeDeclaration(receiverType), null, contextCu, false, null),
                            ResolutionBudget.defaults());
                    if (!resolved.isEmpty()) {
                        return resolved;
                    }
                }
            }
        } else if (exprNode instanceof MethodInvocation mi) {
            String getterName = mi.getName().getIdentifier();
            String propName = methodSuffix != null && !methodSuffix.isEmpty()
                    ? (methodSuffix.startsWith(".get") ? methodSuffix.substring(4) : methodSuffix.substring(1))
                    : getterName;
            Expression receiver = mi.getExpression();
            String receiverName = null;
            String receiverType = null;
            ClassInstanceCreation directCic = null;

            if (receiver instanceof SimpleName sn) {
                receiverName = sn.getIdentifier();
                receiverType = variableTracer.getVariableDeclaredType(scopeMethod, receiverName);
            } else if (receiver instanceof ClassInstanceCreation cic) {
                directCic = cic;
                receiverType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
            } else if (receiver instanceof MethodInvocation receiverMi) {
                GetterChainReceiver chainedReceiver = resolveGetterChainToReceiver(receiverMi, scopeMethod);
                if (chainedReceiver != null) {
                    receiverType = chainedReceiver.typeFqn();
                    directCic = chainedReceiver.cic();
                } else {
                    Expression currentMi = receiverMi;
                    while (currentMi instanceof MethodInvocation chainMi) {
                        String mName = chainMi.getName().getIdentifier();
                        if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
                            if (!chainMi.arguments().isEmpty()) {
                                Expression arg = (Expression) chainMi.arguments().get(0);
                                List<String> builderConstants = new ArrayList<>();
                                constantExtractor.extractConstantsFromExpression(arg, builderConstants);
                                if (!builderConstants.isEmpty()) {
                                    return builderConstants;
                                }
                            }
                        }
                        currentMi = chainMi.getExpression();
                    }
                }
            } else if (receiver instanceof SuperMethodInvocation) {
                int lastDot = entryMethod.lastIndexOf('.');
                String className = lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
                TypeDeclaration currentTd = context.getTypeDeclaration(className);
                if (currentTd != null) {
                    receiverType = context.getSuperclassFqn(currentTd);
                }
            } else if (receiver instanceof FieldAccess fa) {
                receiverName = fa.getName().getIdentifier();
                if (fa.resolveFieldBinding() != null && fa.resolveFieldBinding().getType() != null) {
                    receiverType = fa.resolveFieldBinding().getType().getErasure().getQualifiedName();
                }
            }

            if (receiverType == null && receiverName != null && path.size() > 1) {
                String callerMethod = path.get(1);
                receiverType = variableTracer.getVariableDeclaredType(callerMethod, receiverName);
            }

            if (receiverType == null && receiver instanceof SimpleName) {
                String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
                        mi, constantResolver, context);
                if (remapped == null) {
                    remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
                            mi.toString(), scopeMethod, constantResolver, context);
                }
                if (remapped != null) {
                    return evaluateGetterOnReceiver(remapped, "", entryMethod, path, null);
                }
            }

            if (receiverType != null) {
                String simpleReceiverType = receiverType;
                if (receiverType.contains("<")) {
                    String rawType = receiverType.substring(0, receiverType.indexOf('<'));
                    if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || context.getTypeDeclaration(rawType) == null) {
                        int start = receiverType.indexOf('<');
                        int end = receiverType.lastIndexOf('>');
                        if (start >= 0 && end > start) {
                            simpleReceiverType = receiverType.substring(start + 1, end);
                        }
                    } else {
                        simpleReceiverType = rawType;
                    }
                }
                CompilationUnit contextCu = null;
                int lastDot = entryMethod.lastIndexOf('.');
                if (lastDot > 0) {
                    String className = entryMethod.substring(0, lastDot);
                    TypeDeclaration entryTd = context.getTypeDeclaration(className);
                    if (entryTd != null) {
                        contextCu = (CompilationUnit) entryTd.getRoot();
                    }
                }
                TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType, contextCu);
                if (td == null) {
                    td = context.getTypeDeclaration(simpleReceiverType);
                }
                if (td != null) {
                    Map<String, String> fieldValues = new HashMap<>();
                    String varName = receiverName;
                    ClassInstanceCreation cic = null;
                    if (directCic != null) {
                        cic = directCic;
                    } else {
                        Expression initExpr = findVariableInitializer(scopeMethod, varName);
                        if (initExpr instanceof MethodInvocation initMi) {
                            initExpr = unwrapMethodInvocation(initMi, 0, entryMethod);
                        }
                        if (initExpr instanceof ClassInstanceCreation) {
                            cic = (ClassInstanceCreation) initExpr;
                        }
                    }
                    MethodDeclaration getter = null;
                    boolean anonymousGetter = false;
                    if (cic == null) {
                        String ownerFqn = context.getFqn(td);
                        List<String> resolvedWithoutCic = accessorResolver.resolve(
                                ownerFqn,
                                getterName,
                                new AccessorResolver.ReceiverContext(td, null, contextCu, false, fieldValues),
                                ResolutionBudget.defaults());
                        if (!resolvedWithoutCic.isEmpty()) {
                            return resolvedWithoutCic;
                        }
                    } else {
                        if (cic.getAnonymousClassDeclaration() != null) {
                            for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
                                if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) {
                                    getter = md;
                                    anonymousGetter = true;
                                    break;
                                }
                            }
                        }
                        if (getter == null) {
                            getter = context.findMethodDeclaration(td, getterName, true);
                        }
                        if (getter != null && getter.getBody() != null) {
                            fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(
                                    td, cic, context, this::evaluateMethodCallInConstructor));
                            String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
                            if (result != null) {
                                String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
                                if (returnType != null && !result.contains(".")) {
                                    int lastDotRet = returnType.lastIndexOf('.');
                                    String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
                                    if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer")
                                            && !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) {
                                        result = simpleReturnType + "." + result;
                                    }
                                }
                                return Collections.singletonList(result);
                            }
                        }
                        List<String> resolvedWithCic = accessorResolver.resolve(
                                context.getFqn(td),
                                getterName,
                                new AccessorResolver.ReceiverContext(td, cic, contextCu, anonymousGetter, fieldValues),
                                ResolutionBudget.defaults());
                        if (!resolvedWithCic.isEmpty()) {
                            return resolvedWithCic;
                        }
                    }
                }
            }
        }
        return null;
    }

    private List<String> buildGetterSearchMethods(String resolvedValue, String entryMethod, List<String> path) {
        java.util.LinkedHashSet<String> methods = new java.util.LinkedHashSet<>();
        methods.add(resolveScopeMethodForResolvedValue(resolvedValue, entryMethod, path));
        if (path != null) {
            methods.addAll(path);
        }
        if (entryMethod != null) {
            methods.add(entryMethod);
        }
        return List.copyOf(methods);
    }

    private String resolveScopeMethodForResolvedValue(String resolvedValue, String entryMethod, List<String> path) {
        ASTNode node = parseExpressionString(resolvedValue);
        while (node instanceof ParenthesizedExpression pe) {
            node = pe.getExpression();
        }
        if (node instanceof Expression expression) {
            Expression root = expression;
            while (root instanceof MethodInvocation mi) {
                root = mi.getExpression();
            }
            if (root instanceof SimpleName sn) {
                return resolveScopeMethodForVariable(sn.getIdentifier(), entryMethod, path);
            }
        }
        return entryMethod;
    }

    protected Expression findVariableInitializer(String methodFqn, String varName) {
        int lastDot = methodFqn.lastIndexOf('.');
        if (lastDot < 0) return null;
        String className = methodFqn.substring(0, lastDot);
        String methodName = methodFqn.substring(lastDot + 1);

        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td == null) return null;

        MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
        if (md == null || md.getBody() == null) return null;

        final Expression[] initExpr = {null};
        md.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(VariableDeclarationStatement node) {
                for (Object fragObj : node.fragments()) {
                    VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                    if (frag.getName().getIdentifier().equals(varName) && frag.getInitializer() != null) {
                        initExpr[0] = frag.getInitializer();
                        return false;
                    }
                }
                return super.visit(node);
            }
        });
        return initExpr[0];
    }

    /**
     * Splits a traced argument into a receiver base and a trailing accessor suffix for getter-chain
     * resolution, e.g. {@code wrapper.getEvent()} → {@code ["wrapper", ".getEvent()"]}.
     * <p>
     * Map/collection lookups ({@code ROUTES.get(key)}) stay intact and are resolved by
     * {@link ConstantExtractor}; see {@link ExpressionAccessClassifier} for the type-based distinction
     * from {@code Supplier.get()} and JavaBean accessors.
     */
    protected String[] extractMethodSuffix(String paramName, String currentSuffix) {
        return extractMethodSuffix(paramName, currentSuffix, null);
    }

    protected String[] extractMethodSuffix(String paramName, String currentSuffix, String scopeMethod) {
        if (expressionAccessClassifier.isKeyedLookup(paramName, scopeMethod)) {
            return new String[] { paramName, currentSuffix };
        }
        if (paramName != null && !paramName.contains(" ") && !paramName.startsWith("new ")) {
            int bracketIndex = paramName.indexOf('[');
            int dotIndex = -1;
            int parenDepth = 0;
            for (int i = 0; i < paramName.length(); i++) {
                char c = paramName.charAt(i);
                if (c == '(') parenDepth++;
                else if (c == ')') parenDepth--;
                else if (c == '.' && parenDepth == 0) {
                    dotIndex = i;
                    break;
                }
            }
            
            if (bracketIndex > 0 && (dotIndex == -1 || bracketIndex < dotIndex)) {
                return new String[] { paramName.substring(0, bracketIndex), paramName.substring(bracketIndex) + currentSuffix };
            } else if (dotIndex > 0) {
                if (paramName.startsWith("this.")) {
                    String afterThis = paramName.substring(5);
                    if ((afterThis.startsWith("get") || afterThis.startsWith("is") || afterThis.contains("(")) && !afterThis.contains(".")) {
                        return new String[] { "this", "." + afterThis + currentSuffix };
                    }
                    int nextDotIndex = paramName.indexOf('.', 5);
                    if (nextDotIndex > 0) {
                        return new String[] { paramName.substring(0, nextDotIndex), paramName.substring(nextDotIndex) + currentSuffix };
                    }
                    return new String[] { paramName, currentSuffix };
                }
                return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix };
            }
        }
        return new String[] { paramName, currentSuffix };
    }

    protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
        return unwrapMethodInvocation(mi, depth, null);
    }

    protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
        return MethodInvocationUnwrapper.unwrap(mi, depth, methodFqn, context, miArg -> {
            if (methodFqn == null || !methodFqn.contains(".")) {
                return null;
            }
            String currentClass = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
            if (miArg.getExpression() == null || miArg.getExpression().toString().equals("this")) {
                return currentClass + "." + miArg.getName().getIdentifier();
            }
            return null;
        });
    }

    public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
        return constantExtractor.resolveClassConstantReturns(className, context, contextCu);
    }

    private boolean isExternalParameter(String entryMethod, String paramName) {
        if (entryMethod == null || paramName == null || !entryMethod.contains(".")) return false;
        String className = entryMethod.substring(0, entryMethod.lastIndexOf('.'));
        String methodName = entryMethod.substring(entryMethod.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td != null) {
            MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
            if (md != null) {
                for (Object paramObj : md.parameters()) {
                    if (paramObj instanceof SingleVariableDeclaration svd) {
                        if (svd.getName().getIdentifier().equals(paramName)) {
                            for (Object modifier : svd.modifiers()) {
                                if (modifier instanceof Annotation annotation) {
                                    String typeName = annotation.getTypeName().getFullyQualifiedName();
                                    if (typeName.endsWith("PathVariable") || typeName.endsWith("RequestParam")
                                        || typeName.endsWith("PathParam") || typeName.endsWith("QueryParam")) {
                                        return true;
                                    }
                                    if (typeName.endsWith("RequestBody") || typeName.endsWith("Payload")) {
                                        ITypeBinding typeBinding = svd.getType().resolveBinding();
                                        if (typeBinding != null && typeBinding.isEnum()) {
                                            return false;
                                        }
                                        return true;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    private String extractPathConstraints(
            List<String> path, Map<String, List<CallEdge>> callGraph, Map<String, String> initialBindings) {
        List<String> constraints = new ArrayList<>();
        String bindingConstraint = constraintFromBindings(initialBindings);
        if (bindingConstraint != null) {
            constraints.add(bindingConstraint);
        }
        for (int i = 0; i < path.size() - 1; i++) {
            String caller = path.get(i);
            String target = path.get(i + 1);
            List<CallEdge> edges = callGraph.get(caller);
            if (edges != null) {
                for (CallEdge edge : edges) {
                    if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
                        if (edge.getConstraint() != null && !edge.getConstraint().isEmpty()) {
                            constraints.add(edge.getConstraint());
                            break;
                        }
                    }
                }
            }
        }
        if (constraints.isEmpty()) return null;
        return String.join(" && ", constraints);
    }

    private static String constraintFromBindings(Map<String, String> bindings) {
        if (bindings == null || bindings.isEmpty()) {
            return null;
        }
        List<String> parts = new ArrayList<>();
        for (Map.Entry<String, String> entry : bindings.entrySet()) {
            String value = entry.getValue();
            if (value == null || value.isBlank() || value.contains("(")) {
                continue;
            }
            if ("true".equals(value) || "false".equals(value)) {
                continue;
            }
            parts.add("\"" + value + "\".equalsIgnoreCase(" + entry.getKey() + ")");
        }
        return parts.isEmpty() ? null : String.join(" && ", parts);
    }

    private static Map<String, String> mergeBindings(
            Map<String, String> initialBindings, Map<String, String> tracedBindings) {
        Map<String, String> merged = new HashMap<>();
        if (tracedBindings != null) {
            merged.putAll(tracedBindings);
        }
        if (initialBindings != null) {
            merged.putAll(initialBindings);
        }
        return merged;
    }

    protected String resolveConstraint(MethodInvocation node, String calledMethod, String baseConstraint) {
        String resolvedConstraint = baseConstraint;
        if (node.getExpression() instanceof MethodInvocation miReceiver) {
            if (miReceiver.getName().getIdentifier().equals("get") && !miReceiver.arguments().isEmpty() && miReceiver.getExpression() != null) {
                Expression keyExpr = (Expression) miReceiver.arguments().get(0);
                String mapKeyVar = keyExpr.toString();
                Map<String, String> keyToType = findMapPopulatedKeys(node, miReceiver.getExpression().toString());
                
                int lastDot = calledMethod.lastIndexOf('.');
                if (lastDot > 0) {
                    String impl = calledMethod.substring(0, lastDot);
                    for (Map.Entry<String, String> entry : keyToType.entrySet()) {
                        String registeredType = entry.getValue();
                        if (registeredType.equals(impl) || context.getImplementations(registeredType).contains(impl) || context.getImplementations(impl).contains(registeredType)) {
                            String mapConstraint = mapKeyVar + " == \"" + entry.getKey() + "\"";
                            if (resolvedConstraint != null && !resolvedConstraint.isEmpty()) {
                                resolvedConstraint = resolvedConstraint + " && " + mapConstraint;
                            } else {
                                resolvedConstraint = mapConstraint;
                            }
                            break;
                        }
                    }
                }
            }
        }
        return resolvedConstraint;
    }

    protected String resolveMapValueType(MethodInvocation mi) {
        Expression receiver = mi.getExpression();
        if (receiver == null) return null;
        
        ITypeBinding binding = receiver.resolveTypeBinding();
        if (binding != null && binding.getTypeArguments().length >= 2) {
            ITypeBinding valBinding = binding.getTypeArguments()[1];
            return valBinding.getErasure().getQualifiedName();
        }
        
        if (receiver instanceof SimpleName sn) {
            MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
            if (enclosingMethod != null) {
                for (Object paramObj : enclosingMethod.parameters()) {
                    SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
                    if (svd.getName().getIdentifier().equals(sn.getIdentifier())) {
                        return extractMapValueTypeFromString(svd.getType().toString(), sn);
                    }
                }
            }
            TypeDeclaration enclosingType = findEnclosingType(sn);
            if (enclosingType != null) {
                for (FieldDeclaration fd : enclosingType.getFields()) {
                    for (Object fragObj : fd.fragments()) {
                        if (fragObj instanceof VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(sn.getIdentifier())) {
                            return extractMapValueTypeFromString(fd.getType().toString(), sn);
                        }
                    }
                }
            }
        }
        return null;
    }

    private String extractMapValueTypeFromString(String typeStr, ASTNode contextNode) {
        if (typeStr.contains("<")) {
            String generics = typeStr.substring(typeStr.indexOf('<') + 1, typeStr.lastIndexOf('>'));
            String[] parts = generics.split(",");
            if (parts.length >= 2) {
                String valType = parts[1].trim();
                TypeDeclaration td = context.getTypeDeclaration(valType);
                if (td == null) {
                    CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
                    td = context.getTypeDeclaration(valType, cu);
                }
                if (td != null) {
                    return context.getFqn(td);
                }
                return valType;
            }
        }
        return null;
    }

    protected Map<String, String> findMapPopulatedKeys(ASTNode contextNode, String mapVarName) {
        Map<String, String> keyToType = new HashMap<>();
        TypeDeclaration td = findEnclosingType(contextNode);
        if (td != null) {
            td.accept(new ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    if (node.getName().getIdentifier().equals("put") && node.getExpression() != null) {
                        String exprStr = node.getExpression().toString();
                        if (exprStr.equals(mapVarName) || exprStr.endsWith("." + mapVarName)) {
                            if (node.arguments().size() >= 2) {
                                Expression keyArg = (Expression) node.arguments().get(0);
                                Expression valArg = (Expression) node.arguments().get(1);
                                
                                String keyVal = constantResolver.resolve(keyArg, context);
                                if (keyVal == null) {
                                    keyVal = keyArg.toString().replaceAll("[\"']", "");
                                }
                                
                                String valType = null;
                                ITypeBinding valBinding = valArg.resolveTypeBinding();
                                if (valBinding != null) {
                                    valType = valBinding.getErasure().getQualifiedName();
                                }
                                if (valType == null && valArg instanceof SimpleName sn) {
                                    MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
                                    if (enclosingMethod != null) {
                                        for (Object paramObj : enclosingMethod.parameters()) {
                                            SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
                                            if (svd.getName().getIdentifier().equals(sn.getIdentifier())) {
                                                if (svd.getType().isSimpleType()) {
                                                    valType = ((SimpleType) svd.getType()).getName().getFullyQualifiedName();
                                                } else {
                                                    valType = svd.getType().toString();
                                                }
                                            }
                                        }
                                    }
                                    TypeDeclaration enclosingType = findEnclosingType(sn);
                                    if (enclosingType != null) {
                                        for (FieldDeclaration fd : enclosingType.getFields()) {
                                            for (Object fragObj : fd.fragments()) {
                                                if (fragObj instanceof VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(sn.getIdentifier())) {
                                                    if (fd.getType().isSimpleType()) {
                                                        valType = ((SimpleType) fd.getType()).getName().getFullyQualifiedName();
                                                    } else {
                                                        valType = fd.getType().toString();
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                                if (valType != null && keyVal != null) {
                                    TypeDeclaration valTd = context.getTypeDeclaration(valType);
                                    if (valTd == null) {
                                        CompilationUnit cu = (CompilationUnit) node.getRoot();
                                        valTd = context.getTypeDeclaration(valType, cu);
                                    }
                                    if (valTd != null) {
                                        valType = context.getFqn(valTd);
                                    }
                                    keyToType.put(keyVal, valType);
                                }
                            }
                        }
                    }
                    return super.visit(node);
                }
            });
        }
        return keyToType;
    }

    protected abstract String callGraphCacheKey();

    protected abstract String resolveCalledMethod(MethodInvocation node);

    @SuppressWarnings("unchecked")
    public Map<String, List<CallEdge>> buildCallGraph() {
        Map<String, List<CallEdge>> cached =
                (Map<String, List<CallEdge>>) context.getCache().get(callGraphCacheKey());
        if (cached != null) {
            this.graph = cached;
            return cached;
        }
        graph = new HashMap<>();
        for (CompilationUnit cu : context.getCompilationUnits()) {
            cu.accept(new ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    MethodDeclaration md = findEnclosingMethod(node);
                    if (md != null) {
                        TypeDeclaration td = findEnclosingType(md);
                        if (td != null) {
                            String callerFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
                            List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
                            List<String> args = resolveArguments(node.arguments());
                            String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
                                    .findConditionConstraint(node);
                            String receiver = getReceiverString(node.getExpression());
                            String declaredReceiverType =
                                    resolveReceiverTypeFqn(node.getExpression(), td);
                            String injectedReceiverType =
                                    resolveInjectedReceiverTypeFqn(node.getExpression());
                            if (injectedReceiverType != null) {
                                declaredReceiverType = injectedReceiverType;
                            }
                            for (String calledMethod : calledMethods) {
                                String qualifiedTarget = qualifyCalledMethodWithReceiverType(
                                        calledMethod, declaredReceiverType);
                                String receiverTypeFqn =
                                        refineReceiverTypeForTarget(declaredReceiverType, qualifiedTarget);
                                CallEdge edge = new CallEdge(
                                        qualifiedTarget, args, receiver, null, receiverTypeFqn);
                                edge.setConstraint(resolveConstraint(node, qualifiedTarget, constraint));
                                graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
                            }
                            for (Object argObj : node.arguments()) {
                                if (argObj instanceof ExpressionMethodReference emr) {
                                    String typeName = emr.getExpression().toString();
                                    if ("this".equals(typeName) || "super".equals(typeName)) {
                                        TypeDeclaration refType = findEnclosingType(node);
                                        if (refType != null) {
                                            String refMethod = resolveMethodInType(refType, emr.getName().getIdentifier());
                                            if (refMethod != null) {
                                                List<String> implicitArgs = new ArrayList<>();
                                                if (click.kamil.springstatemachineexporter.ast.common.AstUtils
                                                        .isFunctionalCollectionMethod(node)) {
                                                    implicitArgs.add(node.getExpression().toString());
                                                } else {
                                                    implicitArgs.addAll(args);
                                                }
                                                CallEdge edge = new CallEdge(
                                                        refMethod,
                                                        implicitArgs,
                                                        receiver,
                                                        constraint,
                                                        refineReceiverTypeForTarget(
                                                                declaredReceiverType, refMethod));
                                                graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
                                            }
                                        }
                                    } else {
                                        String fallbackTypeFqn = null;
                                        ITypeBinding binding = emr.getExpression().resolveTypeBinding();
                                        if (binding != null) {
                                            fallbackTypeFqn = binding.getQualifiedName();
                                        } else if (emr.getExpression() instanceof SimpleName sn) {
                                            fallbackTypeFqn = resolveReceiverTypeFallback(sn);
                                        }
                                        if (fallbackTypeFqn != null) {
                                            String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
                                            List<String> implicitArgs = new ArrayList<>();
                                            if (click.kamil.springstatemachineexporter.ast.common.AstUtils
                                                    .isFunctionalCollectionMethod(node)) {
                                                implicitArgs.add(node.getExpression().toString());
                                            } else {
                                                implicitArgs.addAll(args);
                                            }
                                            String emrReceiver = getReceiverString(emr.getExpression());
                                            CallEdge edge = new CallEdge(
                                                    calledMethod,
                                                    implicitArgs,
                                                    emrReceiver,
                                                    constraint,
                                                    refineReceiverTypeForTarget(fallbackTypeFqn, calledMethod));
                                            graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);

                                            List<String> impls = context.getImplementations(fallbackTypeFqn);
                                            for (String impl : impls) {
                                                String implMethod = impl + "." + emr.getName().getIdentifier();
                                                CallEdge implEdge = new CallEdge(
                                                        implMethod,
                                                        args,
                                                        emrReceiver,
                                                        constraint,
                                                        refineReceiverTypeForTarget(fallbackTypeFqn, implMethod));
                                                graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(implEdge);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    return super.visit(node);
                }

                @Override
                public boolean visit(SuperMethodInvocation node) {
                    MethodDeclaration md = findEnclosingMethod(node);
                    if (md != null) {
                        TypeDeclaration tdOuter = findEnclosingType(md);
                        if (tdOuter != null) {
                            String callerFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
                            String methodName = node.getName().getIdentifier();
                            String calledMethod = InheritanceCallTargetResolver.resolveSuperMethod(
                                    context, tdOuter, methodName);
                            if (calledMethod != null) {
                                List<String> args = resolveArguments(node.arguments());
                                String receiver = "super";
                                String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
                                        .findConditionConstraint(node);
                                String receiverTypeFqn = refineReceiverTypeForTarget(
                                        context.getFqn(tdOuter), calledMethod);
                                CallEdge edge = new CallEdge(
                                        calledMethod, args, receiver, constraint, receiverTypeFqn);
                                graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
                            }
                        }
                    }
                    return super.visit(node);
                }
            });
        }
        context.getCache().put(callGraphCacheKey(), graph);
        return graph;
    }

    protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
        Expression receiver = mi.getExpression();
        String receiverType = null;
        if (receiver == null) {
            TypeDeclaration td = findEnclosingType(mi);
            if (td != null) {
                receiverType = context.getFqn(td);
            }
        } else if (receiver instanceof SimpleName sn) {
            receiverType = resolveReceiverTypeFallback(sn);
        } else if (receiver instanceof FieldAccess fa) {
            receiverType = resolveReceiverTypeFallback(fa.getName());
        } else if (receiver instanceof MethodInvocation innerMi) {
            receiverType = resolveMethodInvocationReturnType(innerMi);
        }

        if (receiverType == null) {
            ITypeBinding binding = receiver != null ? receiver.resolveTypeBinding() : null;
            if (binding != null) {
                receiverType = binding.getErasure().getQualifiedName();
            }
        }

        if (receiverType != null) {
            String rawType = receiverType;
            if (rawType.contains("<")) {
                rawType = rawType.substring(0, rawType.indexOf('<'));
            }
            if (rawType.equals("java.util.Map") || rawType.equals("Map")) {
                String valType = resolveMapValueType(mi);
                if (valType != null) {
                    return valType;
                }
            }
            if (receiverType.contains("<")) {
                receiverType = receiverType.substring(0, receiverType.indexOf('<'));
            }
            String methodName = mi.getName().getIdentifier();
            TypeDeclaration td = context.getTypeDeclaration(receiverType);
            if (td == null && receiverType.contains(".")) {
                String simpleClass = receiverType.substring(receiverType.lastIndexOf('.') + 1);
                td = context.getTypeDeclaration(simpleClass);
            }
            if (td != null) {
                MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
                if (md != null && md.getReturnType2() != null) {
                    return resolveTypeToFqn(md.getReturnType2(), mi);
                }
            }
        }

        IMethodBinding methodBinding = mi.resolveMethodBinding();
        if (methodBinding != null && methodBinding.getReturnType() != null) {
            return methodBinding.getReturnType().getErasure().getQualifiedName();
        }

        return null;
    }

    protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
        String baseCalled = resolveCalledMethod(node);
        if (baseCalled == null) return Collections.emptyList();

        if (!baseCalled.contains(".")) {
            return Collections.singletonList(baseCalled);
        }

        // Plain doX(m) and this.doX(m) are the same virtual dispatch on the enclosing
        // instance. Returning early on a null expression skipped subclass expansion for the
        // classic template-method pattern (abstract base calls doPMessage without "this.").
        boolean isImplicitThis =
                node.getExpression() == null || node.getExpression() instanceof ThisExpression;
        if (isImplicitThis && isStaticResolvedMethod(baseCalled)) {
            return Collections.singletonList(baseCalled);
        }

        String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
        String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        String cacheKey = baseCalled + "#" + isImplicitThis;
        List<String> cached = polymorphicCallCache.get(cacheKey);
        if (cached != null) {
            return cached;
        }

        List<String> allResolved = new ArrayList<>();
        List<String> impls = context.getImplementations(className);

        boolean shouldExpand = true;
        if (impls.isEmpty()) {
            shouldExpand = false;
        } else if (td == null) {
            shouldExpand = false;
        } else if (isImplicitThis) {
            // Implicit-this / plain calls: only expand abstract/interface hooks. Concrete
            // methods on an abstract class (the template method itself) keep a single edge to
            // the declaring body — expanding them invents ClassA.processMessage / ClassB...
            // keys that do not exist and cross-wires unrelated subclasses.
            shouldExpand = isAbstractOrInterfaceMethod(td, methodName);
        } else if (!td.isInterface() && !Modifier.isAbstract(td.getModifiers())) {
            shouldExpand = false;
        }

        if (log.isDebugEnabled()) {
            log.debug(
                    "resolveCalledMethodsPolymorphic baseCalled={} isImplicitThis={} className={} td={} impls={} shouldExpand={}",
                    baseCalled,
                    isImplicitThis,
                    className,
                    td == null ? null : context.getFqn(td),
                    impls,
                    shouldExpand);
        }

        if (shouldExpand) {
            boolean isAbstractOrInterface = td.isInterface() || Modifier.isAbstract(td.getModifiers());
            Optional<List<String>> precomputed =
                    context.getPolymorphicMethodExpansionIndex().lookup(className, methodName);
            if (precomputed.isPresent()) {
                if (!isAbstractOrInterface) {
                    allResolved.add(baseCalled);
                }
                allResolved.addAll(precomputed.get());
            } else {
                if (!isAbstractOrInterface) {
                    allResolved.add(baseCalled);
                }
                for (String impl : impls) {
                    allResolved.add(impl + "." + methodName);
                }
            }
        } else {
            allResolved.add(baseCalled);
        }

        List<String> frozen = List.copyOf(allResolved);
        polymorphicCallCache.put(cacheKey, frozen);
        if (log.isDebugEnabled()) {
            log.debug("resolveCalledMethodsPolymorphic allResolved={} frozen={}", allResolved, frozen);
        }
        return frozen;
    }

    private boolean isAbstractOrInterfaceMethod(TypeDeclaration declaringType, String methodName) {
        if (declaringType == null || methodName == null) {
            if (log.isDebugEnabled()) {
                log.debug(
                        "isAbstractOrInterfaceMethod declaringTypeFqn=null md=null modifiers=n/a methodName={} result=false",
                        methodName);
            }
            return false;
        }
        String declaringTypeFqn = context.getFqn(declaringType);
        if (declaringType.isInterface()) {
            if (log.isDebugEnabled()) {
                log.debug(
                        "isAbstractOrInterfaceMethod declaringTypeFqn={} md=n/a (interface) modifiers=n/a methodName={} result=true",
                        declaringTypeFqn,
                        methodName);
            }
            return true;
        }
        MethodDeclaration md = context.findMethodDeclaration(declaringType, methodName, false);
        boolean result = md != null && Modifier.isAbstract(md.getModifiers());
        if (log.isDebugEnabled()) {
            log.debug(
                    "isAbstractOrInterfaceMethod declaringTypeFqn={} mdNull={} modifiers={} methodName={} result={}",
                    declaringTypeFqn,
                    md == null,
                    md == null ? null : md.getModifiers(),
                    methodName,
                    result);
        }
        return result;
    }

    private boolean isStaticResolvedMethod(String methodFqn) {
        int lastDot = methodFqn.lastIndexOf('.');
        if (lastDot <= 0) {
            return false;
        }
        String className = methodFqn.substring(0, lastDot);
        String methodName = methodFqn.substring(lastDot + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td == null) {
            return false;
        }
        MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
        return md != null && Modifier.isStatic(md.getModifiers());
    }

    /**
     * Peels enum factory / library wrappers while tracing call-path arguments, e.g.
     * {@code OrderEvent.valueOf(action)} → {@code action} so the next hop can resolve the parameter.
     * <p>
     * Does <em>not</em> peel implicit-{@code this} domain helpers ({@code toEvent(msg)}, {@code resolve(x)}).
     * Those must stay intact so {@link ConstantExtractor} / {@link AccessorResolver} can widen to
     * subclass implementations and walk overridden return statements. Pure passthrough helpers that
     * literally {@code return arg;} are still peeled via {@link MethodInvocationUnwrapper}.
     * <p>
     * Map/collection lookups ({@code ROUTES.get(key)}) are intentionally <em>not</em> peeled — they are
     * routing tables, not enum converters. See {@link ExpressionAccessClassifier}.
     */
    private String unwrapConverterMethods(String arg, String scopeMethodFqn) {
        if (arg == null) return null;
        String current = arg;
        while (current.contains("(") && !current.startsWith("new ")) {
            ASTNode argNode = parseExpressionString(current);
            if (!(argNode instanceof MethodInvocation miArg) || miArg.arguments().isEmpty()) {
                break;
            }
            if (expressionAccessClassifier.isKeyedLookup(miArg, scopeMethodFqn)) {
                break;
            }
            String methodName = miArg.getName().getIdentifier();
            Expression recv = miArg.getExpression();

            // Type.valueOf(x) / Enum.valueOf(Class, x) — peel to the name argument.
            if ("valueOf".equals(methodName) && isTypeLikeReceiver(recv)) {
                Expression innerArg = (Expression) miArg.arguments().get(0);
                if (miArg.arguments().size() == 2) {
                    innerArg = (Expression) miArg.arguments().get(1);
                }
                current = innerArg.toString();
                continue;
            }

            // Optional.of / Mono.just / MessageBuilder.withPayload — peel library factories only.
            if (recv != null
                    && LibraryUnwrapRegistry.isUnwrapReceiverType(recv.toString())
                    && LibraryUnwrapRegistry.isUnwrapMethodName(methodName)
                    && !LibraryUnwrapRegistry.shouldStopUnwrap(methodName)) {
                current = ((Expression) miArg.arguments().get(0)).toString();
                continue;
            }

            // Implicit-this / this.helper(arg): only peel pure passthroughs (return the same parameter).
            // Domain converters that return enum constants must remain for override/return walking.
            if (recv == null || recv instanceof ThisExpression) {
                Expression peeled = MethodInvocationUnwrapper.unwrap(
                        miArg,
                        0,
                        scopeMethodFqn,
                        context,
                        mi -> {
                            if (scopeMethodFqn == null || !scopeMethodFqn.contains(".")) {
                                return null;
                            }
                            String currentClass = scopeMethodFqn.substring(0, scopeMethodFqn.lastIndexOf('.'));
                            if (mi.getExpression() == null || mi.getExpression() instanceof ThisExpression) {
                                return currentClass + "." + mi.getName().getIdentifier();
                            }
                            return null;
                        });
                if (peeled != null && peeled != miArg) {
                    String next = peeled.toString();
                    if (!next.equals(current)) {
                        current = next;
                        continue;
                    }
                }
            }
            break;
        }
        return current;
    }

    private static boolean isTypeLikeReceiver(Expression recv) {
        if (recv instanceof SimpleName sn) {
            String name = sn.getIdentifier();
            return name != null && !name.isEmpty() && !Character.isLowerCase(name.charAt(0));
        }
        if (recv instanceof QualifiedName qn) {
            String name = qn.getName().getIdentifier();
            return name != null && !name.isEmpty() && !Character.isLowerCase(name.charAt(0));
        }
        return false;
    }

    private boolean shouldDropBarePolymorphicCandidate(String candidate, String expectedType, CodebaseContext context) {
        if (candidate == null || candidate.contains(".")
                || candidate.startsWith("<SYMBOLIC:") || candidate.startsWith("ENUM_SET:")) {
            return false;
        }
        if (!candidate.equals(candidate.toUpperCase()) || candidate.length() <= 1) {
            return true;
        }
        if (expectedType != null) {
            if (context.isAmbiguousSimpleName(expectedType)) {
                return true;
            }
            List<String> enumValues = context.getEnumValues(expectedType);
            if (enumValues == null || enumValues.isEmpty()) {
                return false;
            }
            for (String enumValue : enumValues) {
                if (enumValue.endsWith("." + candidate)) {
                    return false;
                }
            }
            return true;
        }
        int matchingEnumTypes = 0;
        for (List<String> enumValues : context.getEnumValuesMap().values()) {
            for (String enumValue : enumValues) {
                if (enumValue.endsWith("." + candidate)) {
                    matchingEnumTypes++;
                    break;
                }
            }
        }
        return matchingEnumTypes != 1;
    }

    private void qualifyBareEnumConstants(List<String> polymorphicEvents, String expectedType) {
        if (expectedType == null || polymorphicEvents == null || polymorphicEvents.isEmpty()) {
            return;
        }
        List<String> enumValues = context.getEnumValues(expectedType);
        String simpleExpectedType = expectedType;
        if (enumValues == null) {
            simpleExpectedType = expectedType.contains(".")
                    ? expectedType.substring(expectedType.lastIndexOf('.') + 1)
                    : expectedType;
            enumValues = context.getEnumValues(simpleExpectedType);
        } else if (expectedType.contains(".")) {
            simpleExpectedType = expectedType.substring(expectedType.lastIndexOf('.') + 1);
        }
        if (enumValues == null || enumValues.isEmpty()) {
            return;
        }
        for (int i = 0; i < polymorphicEvents.size(); i++) {
            String pe = polymorphicEvents.get(i);
            if (pe == null || pe.contains(".") || pe.startsWith("<SYMBOLIC:") || pe.startsWith("ENUM_SET:")) {
                continue;
            }
            if (!pe.matches("^[A-Z_][A-Z0-9_]*$")) {
                continue;
            }
            boolean matchesEnum = false;
            for (String enumValue : enumValues) {
                if (enumValue.endsWith("." + pe)) {
                    matchesEnum = true;
                    break;
                }
            }
            if (matchesEnum) {
                polymorphicEvents.set(i, simpleExpectedType + "." + pe);
            }
        }
    }

    private String resolveValueOfEnumTypeName(MethodInvocation mi) {
        if (mi == null || !"valueOf".equals(mi.getName().getIdentifier())) {
            return null;
        }
        org.eclipse.jdt.core.dom.IMethodBinding binding = mi.resolveMethodBinding();
        if (binding != null && binding.getDeclaringClass() != null) {
            String fqn = binding.getDeclaringClass().getErasure().getQualifiedName();
            if (isResolvedEnumTypeFqn(fqn)) {
                return fqn;
            }
        }
        Expression receiver = mi.getExpression();
        if (receiver instanceof SimpleName sn) {
            org.eclipse.jdt.core.dom.ITypeBinding typeBinding = sn.resolveTypeBinding();
            if (typeBinding != null) {
                String fqn = typeBinding.getErasure().getQualifiedName();
                if (isResolvedEnumTypeFqn(fqn)) {
                    return fqn;
                }
            }
            return sn.getIdentifier();
        }
        if (receiver instanceof QualifiedName qn) {
            org.eclipse.jdt.core.dom.IBinding nameBinding = qn.resolveBinding();
            if (nameBinding instanceof org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
                String fqn = typeBinding.getErasure().getQualifiedName();
                if (isResolvedEnumTypeFqn(fqn)) {
                    return fqn;
                }
            }
            // Receiver of Type.valueOf(...) IS the enum type. Do not strip the last segment —
            // that turns com.example.DomainCommand into package prefix com.example.
            String full = qn.getFullyQualifiedName();
            if (isResolvedEnumTypeFqn(full)) {
                return full;
            }
            if (full != null && !full.isBlank() && !full.endsWith(".") && !full.startsWith(".")) {
                return full;
            }
            return null;
        }
        return null;
    }

    private static boolean isResolvedEnumTypeFqn(String fqn) {
        if (fqn == null || fqn.isBlank() || fqn.startsWith("<") || fqn.startsWith(".") || fqn.endsWith(".")) {
            return false;
        }
        int lastDot = fqn.lastIndexOf('.');
        String simple = lastDot >= 0 ? fqn.substring(lastDot + 1) : fqn;
        return !simple.isEmpty() && Character.isUpperCase(simple.charAt(0));
    }

    private String reconstructEnumRef(String value) {
        return click.kamil.springstatemachineexporter.analysis.resolver.TruncatedEnumRefReconstructor
                .reconstruct(value, context);
    }

    private String resolveEnumConstantFromArgument(String argName, List<String> path, Map<String, List<CallEdge>> callGraph) {
        if (argName == null || path == null || path.size() < 2) {
            return null;
        }

        for (int i = path.size() - 1; i > 0; i--) {
            String target = path.get(i);
            String caller = path.get(i - 1);

            Map<String, String> parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i);
            String traced = variableTracer.traceLocalVariable(target, argName, parameterValues);
            if (traced != null && !traced.equals(argName)) {
                String normalized = resolveBoundArgumentToEnumConstant(traced, caller);
                if (normalized != null) {
                    return normalized;
                }
            }

            int paramIndex = typeResolver.getParameterIndex(target, argName);
            if (paramIndex >= 0) {
                List<CallEdge> edges = callGraph.get(caller);
                if (edges != null) {
                    for (CallEdge edge : edges) {
                        if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
                            if (paramIndex < edge.getArguments().size()) {
                                String normalized = resolveBoundArgumentToEnumConstant(
                                        edge.getArguments().get(paramIndex), caller);
                                if (normalized != null) {
                                    return normalized;
                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    private String resolveBoundArgumentToEnumConstant(String boundValue, String scopeMethodFqn) {
        if (boundValue == null || boundValue.isBlank()) {
            return null;
        }
        String fromLiteral = normalizeEnumConstantName(boundValue);
        if (fromLiteral != null) {
            return fromLiteral;
        }
        return resolveMethodCallExpressionToEnumConstant(boundValue, scopeMethodFqn);
    }

    private String resolveMethodCallExpressionToEnumConstant(String expr, String scopeMethodFqn) {
        if (expr == null || !expr.contains("(")) {
            return null;
        }
        ASTNode node = parseExpressionString(expr);
        while (node instanceof ParenthesizedExpression pe) {
            node = pe.getExpression();
        }
        if (!(node instanceof MethodInvocation mi) || !mi.arguments().isEmpty()) {
            return null;
        }
        String methodName = mi.getName().getIdentifier();
        String receiverType = null;
        Expression receiver = mi.getExpression();
        if (receiver instanceof SimpleName sn) {
            receiverType = variableTracer.getVariableDeclaredType(scopeMethodFqn, sn.getIdentifier());
        } else if (receiver instanceof FieldAccess fa) {
            receiverType = variableTracer.getVariableDeclaredType(scopeMethodFqn, fa.getName().getIdentifier());
        }
        if (receiverType == null) {
            return null;
        }
        String simpleReceiverType = receiverType.contains("<")
                ? receiverType.substring(0, receiverType.indexOf('<'))
                : receiverType;
        TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType);
        if (td == null) {
            td = context.getTypeDeclaration(receiverType);
        }
        if (td == null) {
            return null;
        }
        String ownerFqn = context.getFqn(td);
        CompilationUnit cu = td.getRoot() instanceof CompilationUnit contextCu ? contextCu : null;
        List<String> constants = resolveAccessorConstants(ownerFqn, methodName, cu);
        if (constants.isEmpty()) {
            return null;
        }
        if (constants.size() == 1) {
            return normalizeEnumConstantName(constants.get(0));
        }
        return null;
    }

    private String normalizeEnumConstantName(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        String trimmed = value.trim();
        if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
            trimmed = trimmed.substring(1, trimmed.length() - 1);
        }
        if (trimmed.contains("(") || trimmed.contains(" ")) {
            return null;
        }
        if (trimmed.contains(".")) {
            trimmed = trimmed.substring(trimmed.lastIndexOf('.') + 1);
        }
        if (!trimmed.matches("[A-Z_][A-Z0-9_]*")) {
            return null;
        }
        return trimmed;
    }

    private boolean hasConcreteEnumConstants(List<String> polymorphicEvents, String declaredType, CodebaseContext context) {
        if (polymorphicEvents == null || polymorphicEvents.isEmpty() || declaredType == null) {
            return false;
        }
        String simpleDeclared = declaredType.contains(".")
                ? declaredType.substring(declaredType.lastIndexOf('.') + 1)
                : declaredType;
        List<String> enumValues = context.getEnumValues(declaredType);

        for (String pe : polymorphicEvents) {
            if (pe == null || pe.startsWith("<SYMBOLIC:")) {
                continue;
            }
            if (pe.contains(".")) {
                String typePart = pe.substring(0, pe.lastIndexOf('.'));
                String simpleType = typePart.contains(".")
                        ? typePart.substring(typePart.lastIndexOf('.') + 1)
                        : typePart;
                if (simpleType.equals(simpleDeclared)) {
                    return true;
                }
                if (enumValues != null && enumValues.contains(pe)) {
                    return true;
                }
            } else if (enumValues != null) {
                for (String ev : enumValues) {
                    if (ev.endsWith("." + pe)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private List<String> resolveLocalSwitchMethodConstants(TypeDeclaration ownerType, MethodInvocation methodInvocation) {
        if (ownerType == null || methodInvocation == null) {
            return List.of();
        }
        MethodDeclaration methodDeclaration = context.findMethodDeclaration(
                ownerType, methodInvocation.getName().getIdentifier(), true);
        if (methodDeclaration == null || methodDeclaration.getBody() == null) {
            return List.of();
        }
        List<String> constants = new ArrayList<>();
        for (Object statementObj : methodDeclaration.getBody().statements()) {
            if (statementObj instanceof SwitchStatement switchStatement) {
                constantExtractor.extractConstantsFromSwitchStatement(switchStatement, constants);
            } else if (statementObj instanceof ReturnStatement returnStatement
                    && returnStatement.getExpression() instanceof SwitchExpression switchExpression) {
                constantExtractor.extractConstantsFromExpression(switchExpression, constants);
            }
        }
        return constants;
    }

    private boolean isRuntimeEnumParameter(Expression exprNode) {
        if (!(exprNode instanceof MethodInvocation mi)) {
            return false;
        }
        if (!"valueOf".equals(mi.getName().getIdentifier()) || mi.arguments().isEmpty()) {
            return false;
        }
        Expression arg = (Expression) mi.arguments().get(0);
        return !(arg instanceof StringLiteral);
    }

    private boolean isExternalTrigger(
            String entryMethod,
            String finalParamName,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            Map<String, String> initialBindings) {
        // Only the resolved event parameter makes a trigger "external". Unrelated REST params
        // (e.g. @RequestParam userId on a dedicated /order/pay endpoint) must not flip the flag.
        return finalParamName != null && isExternalParameter(entryMethod, finalParamName);
    }

    private List<String> resolveKeyedMapLookup(
            MethodInvocation getCall, List<String> path, Map<String, List<CallEdge>> callGraph) {
        List<String> results = new ArrayList<>();
        String literalResult = constantResolver.resolveKeyedMapLookup(getCall, context);
        if (literalResult != null && !literalResult.isBlank()) {
            addResolvedConstantToList(literalResult, results);
            return results;
        }
        if (getCall.arguments().isEmpty()) {
            return results;
        }
        Expression keyArg = (Expression) getCall.arguments().get(0);
        String resolvedKey = resolveStringArgumentFromPath(keyArg, path, callGraph);
        if (resolvedKey != null && getCall.getExpression() != null) {
            String mapValue = resolveKeyedValueFromMapExpression(getCall.getExpression(), resolvedKey);
            if (mapValue != null) {
                addResolvedConstantToList(mapValue, results);
            }
        }
        return results;
    }

    private String resolveStringArgumentFromPath(
            Expression keyArg, List<String> path, Map<String, List<CallEdge>> callGraph) {
        if (keyArg instanceof StringLiteral sl) {
            return sl.getLiteralValue();
        }
        if (keyArg instanceof SimpleName sn) {
            Map<String, String> bindings = resolvePathBindings(path, callGraph, Map.of());
            String bound = bindings.get(sn.getIdentifier());
            if (bound != null) {
                return unwrapStringLiteral(bound);
            }
            for (int i = path.size() - 1; i >= 0; i--) {
                String traced = variableTracer.traceLocalVariable(path.get(i), sn.getIdentifier());
                if (traced != null && !traced.equals(sn.getIdentifier())) {
                    String unwrapped = unwrapStringLiteral(traced);
                    if (unwrapped != null && !unwrapped.isBlank()) {
                        return unwrapped;
                    }
                }
            }
        }
        String resolved = constantResolver.resolve(keyArg, context);
        return unwrapStringLiteral(resolved);
    }

    private boolean hasPathBindingForMapKey(
            MethodInvocation getCall, List<String> path, Map<String, List<CallEdge>> callGraph) {
        if (getCall.arguments().isEmpty()) {
            return false;
        }
        Expression keyArg = (Expression) getCall.arguments().get(0);
        if (!(keyArg instanceof SimpleName sn)) {
            return false;
        }
        Map<String, String> bindings = resolvePathBindings(path, callGraph, Map.of());
        if (bindings.containsKey(sn.getIdentifier())) {
            return true;
        }
        for (String methodFqn : path) {
            String traced = variableTracer.traceLocalVariable(methodFqn, sn.getIdentifier());
            if (traced != null && !traced.equals(sn.getIdentifier()) && unwrapStringLiteral(traced) != null) {
                return true;
            }
        }
        return false;
    }

    private void extractEventConstantsFromConstructor(ClassInstanceCreation cic, List<String> polymorphicEvents) {
        if (cic.getAnonymousClassDeclaration() != null || cic.arguments().isEmpty()) {
            return;
        }
        constantExtractor.extractConstantsFromExpression((Expression) cic.arguments().get(0), polymorphicEvents);
    }

    private void addConstantsFromTracedExpression(
            Expression expr, List<String> constants, List<String> path, Map<String, List<CallEdge>> callGraph, String scopeMethod) {
        List<Expression> traced = variableTracer.traceVariableAll(expr);
        for (Expression tracedExpr : traced) {
            addConstantsFromSingleExpression(tracedExpr, constants, path, callGraph, scopeMethod);
        }
        if (constants.isEmpty()) {
            addConstantsFromSingleExpression(expr, constants, path, callGraph, scopeMethod);
        }
    }

    private void addConstantsFromSingleExpression(
            Expression expr, List<String> constants, List<String> path, Map<String, List<CallEdge>> callGraph, String scopeMethod) {
        if (expr instanceof MethodInvocation mi && expressionAccessClassifier.isKeyedLookup(mi, scopeMethod)) {
            List<String> keyed = resolveKeyedMapLookup(mi, path, callGraph);
            if (!keyed.isEmpty()) {
                for (String keyedValue : keyed) {
                    if (!constants.contains(keyedValue)) {
                        constants.add(keyedValue);
                    }
                }
                return;
            }
            if (!hasPathBindingForMapKey(mi, path, callGraph)) {
                constantExtractor.extractConstantsFromExpression(expr, constants);
            }
            return;
        }
        constantExtractor.extractConstantsFromExpression(expr, constants);
    }

    private List<String> narrowPolymorphicEventsWithPathBindings(
            List<String> polymorphicEvents,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            TriggerPoint tp) {
        if (polymorphicEvents == null) {
            return polymorphicEvents;
        }
        // Shared EI expand consult — ACE only publishes ENUM_EXPAND via shared policy.
        // UNIQUE without hint stays on tryRecoverAllocationSensitiveInto (fail-closed disagree).
        ExpandConsult consult = tryExpandViaEventIdentity(path, callGraph);
        List<String> fromEi = EventIdentityPolyPublish.adoptExpandOrEmpty(consult);
        boolean eiExpand = !fromEi.isEmpty();
        if (polymorphicEvents.isEmpty()) {
            if (eiExpand) {
                return fromEi;
            }
            return polymorphicEvents;
        }
        Map<String, String> bindings = resolvePathBindings(path, callGraph, Map.of());
        if (bindings.isEmpty()) {
            boolean symbolicOnly = polymorphicEvents.stream().noneMatch(this::looksLikeEnumConstant);
            if (eiExpand && symbolicOnly) {
                return fromEi;
            }
            return polymorphicEvents;
        }
        List<String> narrowed = new ArrayList<>(polymorphicEvents);
        if (narrowed.stream().noneMatch(this::looksLikeEnumConstant)) {
            List<String> bindingResolved = resolvePolymorphicEventsFromBindings(bindings, path);
            boolean bindingHasConcrete = bindingResolved.stream().anyMatch(this::looksLikeEnumConstant);
            if (bindingHasConcrete) {
                narrowed = new ArrayList<>();
                for (String candidate : bindingResolved) {
                    addConstantPreferQualified(narrowed, candidate);
                }
            } else {
                for (String candidate : bindingResolved) {
                    addConstantPreferQualified(narrowed, candidate);
                }
            }
        }
        boolean originallySymbolicOnly = polymorphicEvents.stream().noneMatch(this::looksLikeEnumConstant);
        if (originallySymbolicOnly
                && narrowed.stream().filter(this::looksLikeEnumConstant).count() <= 1
                && eiExpand) {
            for (String candidate : fromEi) {
                addConstantPreferQualified(narrowed, candidate);
            }
        }
        boolean hasConcrete = narrowed.stream().anyMatch(this::looksLikeEnumConstant);
        if (hasConcrete) {
            narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
        }
        if (tp.isExternal() && hasConcrete) {
            narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
        }
        narrowed.removeIf(pe -> pe != null && (pe.equals("*>") || pe.equals(".*>")));
        return narrowed;
    }

    /** Merge bare/qualified enum constants without double-counting. */
    private void addConstantPreferQualified(List<String> target, String candidate) {
        if (candidate == null || candidate.isBlank()) {
            return;
        }
        for (int i = 0; i < target.size(); i++) {
            if (EventIdentityNormalize.sameConstant(target.get(i), candidate)) {
                target.set(i, EventIdentityNormalize.preferQualified(target.get(i), candidate, null));
                return;
            }
        }
        target.add(candidate);
    }

    /**
     * Shared EI expand for hint-gated unbound valueOf / string transforms / helper returns.
     */
    private ExpandConsult tryExpandViaEventIdentity(
            List<String> path,
            Map<String, List<CallEdge>> callGraph) {
        if (eventIdentityResolver == null || path == null || path.size() < 2) {
            return ExpandConsult.empty();
        }
        String callerMethod = path.get(path.size() - 2);
        String targetMethod = path.get(path.size() - 1);
        String callerClass = callerMethod.substring(0, callerMethod.lastIndexOf('.'));
        String callerMethodName = callerMethod.substring(callerMethod.lastIndexOf('.') + 1);
        String targetMethodName = targetMethod.substring(targetMethod.lastIndexOf('.') + 1);
        TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
        if (callerType == null) {
            return ExpandConsult.empty();
        }
        MethodDeclaration callerDeclaration = context.findMethodDeclaration(callerType, callerMethodName, true);
        if (callerDeclaration == null || callerDeclaration.getBody() == null) {
            return ExpandConsult.empty();
        }
        Expression[] eventArg = {null};
        callerDeclaration.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if (!targetMethodName.equals(node.getName().getIdentifier()) || node.arguments().isEmpty()) {
                    return super.visit(node);
                }
                eventArg[0] = EventArgumentSelector.selectEventArgument(node).orElse(null);
                return eventArg[0] == null;
            }
        });
        if (eventArg[0] == null) {
            return ExpandConsult.empty();
        }
        Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
        return eventIdentityResolver.resolveExpandConsult(eventArg[0], path, graphForFacts);
    }

    private boolean hasUniqueConcretePolymorphic(List<String> polymorphicEvents) {
        if (polymorphicEvents == null || polymorphicEvents.size() != 1) {
            return false;
        }
        return looksLikeEnumConstant(polymorphicEvents.get(0));
    }

    private List<String> resolvePolymorphicEventsFromBindings(
            Map<String, String> bindings,
            List<String> path) {
        if (bindings == null || bindings.isEmpty() || path == null || path.isEmpty()) {
            return List.of();
        }
        String callerMethod = path.size() >= 2 ? path.get(path.size() - 2) : path.get(0);
        String callerClass = callerMethod.contains(".")
                ? callerMethod.substring(0, callerMethod.lastIndexOf('.'))
                : callerMethod;
        TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
        List<String> resolved = new ArrayList<>();
        for (String boundValue : bindings.values()) {
            if (boundValue == null || boundValue.isBlank()) {
                continue;
            }
            if (boundValue.startsWith("ENUM_SET:")
                    || boundValue.startsWith("<SYMBOLIC:")
                    || boundValue.contains("SYMBOLIC:")
                    || boundValue.endsWith(".*>")) {
                addResolvedConstantToList(boundValue, resolved);
                continue;
            }
            ASTNode parsed = parseExpressionString(boundValue);
            if (!(parsed instanceof Expression boundExpr)) {
                addResolvedConstantToList(boundValue, resolved);
                continue;
            }
            String directResolved = constantResolver.resolve(boundExpr, context);
            if (directResolved != null) {
                addResolvedConstantToList(directResolved, resolved);
            }
            List<String> extracted = new ArrayList<>();
            if (boundExpr instanceof MethodInvocation methodInvocation
                    && callerType != null
                    && (methodInvocation.getExpression() == null
                    || methodInvocation.getExpression() instanceof ThisExpression)) {
                extracted.addAll(resolveLocalSwitchMethodConstants(callerType, methodInvocation));
            }
            if (extracted.isEmpty()) {
                constantExtractor.extractConstantsFromExpression(boundExpr, extracted);
            }
            for (String candidate : extracted) {
                addResolvedConstantToList(candidate, resolved);
            }
        }
        return resolved.stream().distinct().collect(java.util.stream.Collectors.toList());
    }

    private String resolveKeyedValueFromMapExpression(Expression mapExpr, String key) {
        if (mapExpr == null || key == null) {
            return null;
        }
        return constantResolver.resolveKeyedMapLookup(mapExpr, key, context);
    }

    private static String unwrapStringLiteral(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length() >= 2) {
            return trimmed.substring(1, trimmed.length() - 1);
        }
        return trimmed;
    }

    private boolean looksLikeEnumConstant(String value) {
        if (value == null || value.isBlank() || value.startsWith("<SYMBOLIC:")) {
            return false;
        }
        String name = value.contains(".") ? value.substring(value.lastIndexOf('.') + 1) : value;
        return name.matches("[A-Z_][A-Z0-9_]*");
    }

    private void trackKeyedMapLookupFlags(
            Expression expr,
            List<String> path,
            Map<String, List<CallEdge>> callGraph,
            String scopeMethod,
            boolean[] keyedMapLookupOnInitializer,
            boolean[] pathBoundMapKeyOnInitializer) {
        if (expr instanceof MethodInvocation mi && expressionAccessClassifier.isKeyedLookup(mi, scopeMethod)) {
            keyedMapLookupOnInitializer[0] = true;
            if (hasPathBindingForMapKey(mi, path, callGraph)) {
                pathBoundMapKeyOnInitializer[0] = true;
            }
        }
    }

    private void addResolvedConstantToList(String resolved, List<String> results) {
        if (resolved == null || resolved.isBlank()) {
            return;
        }
        if (resolved.startsWith("ENUM_SET:")) {
            for (String part : resolved.substring(9).split(",")) {
                String parsed = constantExtractor.parseEnumSetElement(part);
                if (!results.contains(parsed)) {
                    results.add(parsed);
                }
            }
            return;
        }
        if (resolved.startsWith("<SYMBOLIC:") || resolved.contains("SYMBOLIC:") || resolved.endsWith(".*>")) {
            if (!results.contains(resolved)) {
                results.add(resolved);
            }
            return;
        }
        String parsed = constantExtractor.parseEnumSetElement(resolved);
        if (!results.contains(parsed)) {
            results.add(parsed);
        }
    }
}