package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
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 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;
    private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();

    private ASTNode parseExpressionString(String expr) {
        if (expr == null) return null;
        return parsedNodeCache.computeIfAbsent(expr, e -> {
            ASTParser parser = ASTParser.newParser(AST.JLS17);
            Map<String, String> options = org.eclipse.jdt.core.JavaCore.getOptions();
            org.eclipse.jdt.core.JavaCore.setComplianceOptions(org.eclipse.jdt.core.JavaCore.VERSION_17, options);
            parser.setCompilerOptions(options);
            parser.setKind(ASTParser.K_EXPRESSION);
            parser.setSource(e.toCharArray());
            try {
                ASTNode node = parser.createAST(null);
                if (node instanceof org.eclipse.jdt.core.dom.CompilationUnit) {
                    ASTParser fallbackParser = ASTParser.newParser(AST.JLS17);
                    fallbackParser.setCompilerOptions(options);
                    fallbackParser.setKind(ASTParser.K_COMPILATION_UNIT);
                    fallbackParser.setSource(("class A { Object o = " + e + "; }").toCharArray());
                    org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) fallbackParser.createAST(null);
                    if (!cu.types().isEmpty()) {
                        org.eclipse.jdt.core.dom.TypeDeclaration td = (org.eclipse.jdt.core.dom.TypeDeclaration) cu.types().get(0);
                        if (!td.bodyDeclarations().isEmpty() && td.bodyDeclarations().get(0) instanceof org.eclipse.jdt.core.dom.FieldDeclaration fd) {
                            if (!fd.fragments().isEmpty() && fd.fragments().get(0) instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf) {
                                return vdf.getInitializer();
                            }
                        }
                    }
                }
                return node;
            } catch (Exception ex) {
                return null;
            }
        });
    }

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

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

        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.constantExtractor.setVariableTracer(this.variableTracer);
        this.constantExtractor.setConstructorAnalyzer(this.constructorAnalyzer);
        this.constructorAnalyzer.setVariableTracer(this.variableTracer);
        this.constructorAnalyzer.setConstantExtractor(this.constantExtractor);
    }

    public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
        Map<String, List<CallEdge>> callGraph = buildCallGraph();
        List<CallChain> chains = new ArrayList<>();

        for (EntryPoint ep : entryPoints) {
            String startMethod = ep.getClassName() + "." + ep.getMethodName();
            List<String> startMethods = new ArrayList<>();
            startMethods.add(startMethod);

            boolean foundAny = false;
            for (TriggerPoint tp : triggers) {
                String targetMethod = tp.getClassName() + "." + tp.getMethodName();
                List<List<String>> allPaths = new ArrayList<>();
                for (String sMethod : startMethods) {
                    allPaths.addAll(pathFinder.findAllPaths(sMethod, targetMethod, callGraph, new HashSet<>()));
                }
                Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
                for (List<String> path : uniquePaths) {
                    foundAny = true;
                    TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
                    if (resolvedTp != null) {
                        String contextMachineId = pathFinder.extractContextMachineId(path, callGraph);
                        chains.add(CallChain.builder()
                                .entryPoint(ep)
                                .triggerPoint(resolvedTp)
                                .methodChain(path)
                                .contextMachineId(contextMachineId)
                                .build());
                    }
                }
            }
            if (!foundAny && log.isDebugEnabled()) {
                log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
            }
        }
        return chains;
    }

    protected TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
        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);
        if (resolved == null) return null;
        String entryMethod = path.isEmpty() ? (tp.getClassName() + "." + tp.getMethodName()) : path.get(0);
        boolean isExternal = isExternalParameter(entryMethod, finalParamNameRef[0]);
        
        String pathConstraint = extractPathConstraints(path, callGraph);
        String triggerConstraint = resolved.getConstraint();
        String finalConstraint = null;
        if (pathConstraint != null && triggerConstraint != null) {
            finalConstraint = pathConstraint + " && " + triggerConstraint;
        } else if (pathConstraint != null) {
            finalConstraint = pathConstraint;
        } else {
            finalConstraint = triggerConstraint;
        }
        
        return resolved.toBuilder().external(isExternal).constraint(finalConstraint).build();
    }

    protected TriggerPoint resolveTriggerPointParametersOriginal(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph, String[] finalParamNameRef) {
        click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(path);
        try {
            String event = tp.getEvent();
            event = unwrapConverterMethods(event);
            String currentParamName = event;
            String resolvedValue = event;
            String methodSuffix = "";
            String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix);
            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);
            int paramIndex = typeResolver.getParameterIndex(target, currentParamName);
            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 = 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);
                        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.getParameterType(target, paramIndex);
                        if (expectedType != null && paramIndex < edge.getArguments().size()) {
                            String argValue = edge.getArguments().get(paramIndex);
                            String actualType = null;
                            if (argValue.contains(".") && !argValue.contains("(")) {
                                String prefix = argValue.substring(0, argValue.lastIndexOf('.'));
                                if (!prefix.equals("this") && !prefix.equals("super")) {
                                    actualType = prefix;
                                }
                            }
                            if (actualType == 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);
                                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);
                                 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);
        int entryParamIndex = typeResolver.getParameterIndex(entryMethod, currentParamName);
        if (entryParamIndex < 0) {
            if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
                String localMethodName = methodSuffix.substring(1).replace("()", "");
                Expression localSetterExpr = variableTracer.traceLocalSetter(entryMethod, currentParamName, localMethodName);
                if (localSetterExpr != null) {
                    List<String> setterEvents = new ArrayList<>();
                    List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
                    for (Expression tracedSetter : tracedSetters) {
                        constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
                    }
                    if (!setterEvents.isEmpty()) {
                        log.debug("Early return 1: setterEvents = {}", setterEvents);
                        return TriggerPoint.builder()
                            .event(tp.getEvent())
                            .className(tp.getClassName())
                            .methodName(tp.getMethodName())
                            .sourceFile(tp.getSourceFile())
                            .sourceModule(tp.getSourceModule())
                            .stateMachineId(tp.getStateMachineId())
                            .sourceState(tp.getSourceState())
                            .lineNumber(tp.getLineNumber())
                            .polymorphicEvents(setterEvents)
                            .constraint(tp.getConstraint())
                            .stateTypeFqn(tp.getStateTypeFqn())
                            .eventTypeFqn(tp.getEventTypeFqn())
                            .external(tp.isExternal())
                            .build();
                    }
                }
            }

            boolean hasGetterOnReceiver = (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) && 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()) {
                    log.debug("Early return 2: getterEvents = {}", getterEvents);
                    return TriggerPoint.builder()
                        .event(tp.getEvent())
                        .className(tp.getClassName())
                        .methodName(tp.getMethodName())
                        .sourceFile(tp.getSourceFile())
                        .sourceModule(tp.getSourceModule())
                        .stateMachineId(tp.getStateMachineId())
                        .sourceState(tp.getSourceState())
                        .lineNumber(tp.getLineNumber())
                        .polymorphicEvents(getterEvents)
                        .constraint(tp.getConstraint())
                        .stateTypeFqn(tp.getStateTypeFqn())
                        .eventTypeFqn(tp.getEventTypeFqn())
                        .external(tp.isExternal())
                        .build();
                }
            }

            String tracedVar = variableTracer.traceLocalVariable(entryMethod, currentParamName);
            if (tracedVar != null && !tracedVar.equals(currentParamName)) {
                String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
                tracedVar = extractedFinalTraced[0];
                methodSuffix = extractedFinalTraced[1];
                currentParamName = tracedVar;
                if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName;
                resolvedValue = tracedVar + methodSuffix;
            }
        }
        
        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(tp.getEvent())
                    .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())
                    .build();
        }
        
        ASTNode exprNode = parseExpressionString(resolvedValue);
        while (exprNode instanceof ParenthesizedExpression pe) {
            exprNode = pe.getExpression();
        }
        
        String varName = null;
        String methodName = null;
        String declaredType = null;
        String sourceMethod = null;

        if (exprNode instanceof MethodInvocation mi) {
            methodName = mi.getName().getIdentifier();
            boolean handledStaticEnum = false;
            if (methodName.equals("valueOf") && mi.arguments().size() == 1) {
                Object arg = mi.arguments().get(0);
                if (arg instanceof StringLiteral sl) {
                    polymorphicEvents.add(sl.getLiteralValue());
                    methodName = null;
                    handledStaticEnum = true;
                } else if (arg instanceof QualifiedName qn) {
                    polymorphicEvents.add(qn.getName().getIdentifier());
                    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(tp.getEvent())
                            .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";
            polymorphicEvents.add(declaredType); 
        } else if (exprNode instanceof ConditionalExpression cond) {
            List<Expression> branches = new ArrayList<>();
            branches.add(cond.getThenExpression());
            branches.add(cond.getElseExpression());
            for (Expression branch : branches) {
                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) {
            List<String> polyEventsRef = polymorphicEvents;
            swExpr.accept(new ASTVisitor() {
                @Override
                public boolean visit(YieldStatement ys) {
                    List<Expression> tracedBranch = variableTracer.traceVariableAll(ys.getExpression());
                    for (Expression tb : tracedBranch) {
                        if (tb instanceof ClassInstanceCreation cic) {
                            polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
                        } else {
                            constantExtractor.extractConstantsFromExpression(tb, polyEventsRef);
                        }
                    }
                    return super.visit(ys);
                }
                @Override
                public boolean visit(ExpressionStatement es) {
                    List<Expression> tracedBranch = variableTracer.traceVariableAll(es.getExpression());
                    for (Expression tb : tracedBranch) {
                        if (tb instanceof ClassInstanceCreation cic) {
                            polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
                        } else {
                            constantExtractor.extractConstantsFromExpression(tb, polyEventsRef);
                        }
                    }
                    return super.visit(es);
                }
            });
            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";
            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";
            polymorphicEvents.add(declaredType);
        }
        
        if (methodName != null) {
            if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) {
                Expression localSetterExpr = variableTracer.traceLocalSetter(entryMethod, 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) {
                                        List<Expression> traced = variableTracer.traceVariableAll((Expression) newNode);
                                        for (Expression ex : traced) {
                                            constantExtractor.extractConstantsFromExpression(ex, polymorphicEvents);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                
                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;
                            List<String> values = constantExtractor.resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), 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) {
                                        List<Expression> mapTraced = variableTracer.traceVariableAll((Expression) mapNode);
                                        for (Expression t : mapTraced) {
                                            constantExtractor.extractConstantsFromExpression(t, polymorphicEvents);
                                        }
                                    }
                                }
                            }
                            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;
                        List<String> values = constantExtractor.resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu);
                        if (values != null && !values.isEmpty()) {
                            polymorphicEvents.addAll(values);
                            sourceMethod = methodFqn;
                            break;
                        }
                    }
                }
            }

            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()) {
                    for (String ev : enumValues) {
                        polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
                    }
                    isAmbiguous = true;
                } 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) {
                        Set<String> visited = new HashSet<>();
                        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 = constantExtractor.resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
                        for (String constant : constants) {
                            if (!polymorphicEvents.contains(constant)) {
                                polymorphicEvents.add(constant);
                            }
                        }
                    }
                }
            }
        }

        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) {
            constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents);
        }
        
        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;
        
        polymorphicEvents.removeIf(e -> {
            if (e.contains(".")) return false; 
            String val = e;
            boolean isKnownEnumVal = false;
            for (List<String> vals : context.getEnumValuesMap().values()) {
                for (String v : vals) {
                    if (v.endsWith("." + val)) {
                        isKnownEnumVal = true;
                        break;
                    }
                }
                if (isKnownEnumVal) break;
            }
            if (isKnownEnumVal) return false;
            return !val.equals(val.toUpperCase()) || val.length() <= 1;
        });

        String targetMethod = path.get(path.size() - 1);
        int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event);
        if (eventParamIndex < 0) {
            eventParamIndex = 0;
        }
        String expectedType = typeResolver.getParameterType(targetMethod, eventParamIndex);
        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;
            });
        }
        
        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:")) {
                    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());
        
        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())
                    .build();
        }
        
        return tp;
        } finally {
            click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.clearCurrentPath();
        }
    }

    protected MethodDeclaration findEnclosingMethod(ASTNode node) {
        ASTNode parent = node.getParent();
        while (parent != null && !(parent instanceof MethodDeclaration)) {
            parent = parent.getParent();
        }
        return (MethodDeclaration) parent;
    }

    protected TypeDeclaration findEnclosingType(ASTNode node) {
        ASTNode parent = node.getParent();
        while (parent != null && !(parent instanceof TypeDeclaration)) {
            parent = parent.getParent();
        }
        return (TypeDeclaration) parent;
    }

    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 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) {
        return resolvedValue;
    }

    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);
        while (superFqn != null) {
            TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
            if (superTd != null) {
                resolved = resolveMethodInType(superTd, methodName);
                if (resolved != null) {
                    return resolved;
                }
                superFqn = context.getSuperclassFqn(superTd);
            } else {
                break;
            }
        }
        return null;
    }

    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 List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
        ASTNode exprNode = parseExpressionString(resolvedValue);
        while (exprNode instanceof ParenthesizedExpression pe) {
            exprNode = pe.getExpression();
        }

        if (exprNode instanceof SuperMethodInvocation smi) {
            String getterName = smi.getName().getIdentifier();
            int lastDot = entryMethod.lastIndexOf('.');
            String className = lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
            TypeDeclaration currentTd = context.getTypeDeclaration(className);
            if (currentTd != null) {
                String receiverType = context.getSuperclassFqn(currentTd);
                return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), null);
            }
        } else if (exprNode instanceof MethodInvocation mi) {
            String getterName = mi.getName().getIdentifier();
            String propName = methodSuffix.startsWith(".get") ? methodSuffix.substring(4) : methodSuffix.substring(1);
            Expression receiver = mi.getExpression();
            String receiverName = null;
            String receiverType = null;
            ClassInstanceCreation directCic = null;

            if (receiver instanceof SimpleName sn) {
                receiverName = sn.getIdentifier();
                receiverType = variableTracer.getVariableDeclaredType(entryMethod, receiverName);
            } else if (receiver instanceof ClassInstanceCreation cic) {
                directCic = cic;
                receiverType = cic.getType().toString();
            } else if (receiver instanceof MethodInvocation receiverMi) {
                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);
                }
            }

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

            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(entryMethod, varName);
                        if (initExpr instanceof MethodInvocation initMi) {
                            initExpr = unwrapMethodInvocation(initMi, 0, entryMethod);
                        }
                        if (initExpr instanceof ClassInstanceCreation) {
                            cic = (ClassInstanceCreation) initExpr;
                        }
                    }
                    if (cic == null) {
                        return null;
                    }
                    MethodDeclaration getter = null;
                    if (cic.getAnonymousClassDeclaration() != null) {
                        for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
                            if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) {
                                getter = md;
                                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));
                        int lastDotEntry = entryMethod.lastIndexOf('.');
                        if (lastDotEntry > 0) {
                            String className = entryMethod.substring(0, lastDotEntry);
                            String methodName = entryMethod.substring(lastDot + 1);
                            TypeDeclaration entryTd = context.getTypeDeclaration(className);
                            if (entryTd != null) {
                                MethodDeclaration entryMd = context.findMethodDeclaration(entryTd, methodName, false);
                                if (entryMd != null && entryMd.getBody() != null) {
                                    // Skip trackSetterCallsBetween
                                }
                            }
                        }
                        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);
                        }
                    }
                }
            }
        }
        return null;
    }

    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];
    }

    protected String[] extractMethodSuffix(String paramName, String 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 };
    }

    private int getReturnedParameterIndex(MethodDeclaration md) {
        if (md.getBody() == null) return -1;
        List<?> parameters = md.parameters();
        if (parameters.isEmpty()) return -1;
        
        final List<String> returnedExprs = new ArrayList<>();
        md.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement node) {
                if (node.getExpression() != null) {
                    returnedExprs.add(node.getExpression().toString());
                }
                return super.visit(node);
            }
        });
        
        if (returnedExprs.size() == 1) {
            String retStr = returnedExprs.get(0);
            for (int i = 0; i < parameters.size(); i++) {
                if (parameters.get(i) instanceof SingleVariableDeclaration svd) {
                    if (svd.getName().getIdentifier().equals(retStr)) {
                        return i;
                    }
                }
            }
        }
        return -1;
    }

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

    protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
        if (depth > 5) return mi;
        if (mi.getExpression() != null) {
            String exprStr = mi.getExpression().toString();
            if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
                return mi;
            }
        } else {
            if (methodFqn != null && methodFqn.contains(".")) {
                String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
                TypeDeclaration td = context.getTypeDeclaration(className);
                if (td != null) {
                    MethodDeclaration localMd = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
                    if (localMd != null) {
                        int paramIdx = getReturnedParameterIndex(localMd);
                        if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
                            Expression arg = (Expression) mi.arguments().get(paramIdx);
                            if (arg instanceof MethodInvocation innerMi) {
                                return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
                            }
                            return arg;
                        }
                    }
                }
            }
            return mi;
        }
        
        if (!mi.arguments().isEmpty()) {
            String mName = mi.getName().getIdentifier();
            String lowerName = mName.toLowerCase();
            
            if (lowerName.startsWith("map") || lowerName.startsWith("parse") || 
                lowerName.startsWith("convert") || lowerName.startsWith("to") || 
                lowerName.startsWith("resolve") || lowerName.startsWith("build") || 
                lowerName.startsWith("create") || lowerName.startsWith("from") ||
                lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
                lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
                lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
                lowerName.startsWith("extract") || lowerName.startsWith("get") ||
                lowerName.startsWith("find")) {
                return mi;
            }
            
            Expression arg = (Expression) mi.arguments().get(0);
            if (arg instanceof MethodInvocation innerMi) {
                return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
            }
            return arg;
        }
        return mi;
    }

    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("RequestBody") || typeName.endsWith("RequestParam")
                                        || typeName.endsWith("PathParam") || typeName.endsWith("QueryParam")) {
                                        return true;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    private String extractPathConstraints(List<String> path, Map<String, List<CallEdge>> callGraph) {
        List<String> constraints = new ArrayList<>();
        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);
    }

    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 Map<String, List<CallEdge>> buildCallGraph();
    protected abstract String resolveCalledMethod(MethodInvocation node);

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

        List<String> allResolved = new ArrayList<>();

        if (node.getExpression() == null) {
            return Collections.singletonList(baseCalled);
        }

        if (baseCalled.contains(".")) {
            String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
            String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
            
            TypeDeclaration td = context.getTypeDeclaration(className);
            List<String> impls = context.getImplementations(className);
            
            boolean shouldExpand = true;
            if (impls.isEmpty()) {
                shouldExpand = false;
            } else if (td == null) {
                // Do not polymorphically expand external generic types (e.g. java.lang.Runnable, Spring Action)
                // across the entire codebase. This prevents massive false-positive tangling.
                shouldExpand = false;
            } else {
                boolean isImplicitThis = node.getExpression() instanceof ThisExpression;
                if (!td.isInterface() && !Modifier.isAbstract(td.getModifiers()) && isImplicitThis) {
                    shouldExpand = false;
                }
            }

            if (shouldExpand) {
                boolean isAbstractOrInterface = td.isInterface() || Modifier.isAbstract(td.getModifiers());
                if (!isAbstractOrInterface) {
                    allResolved.add(baseCalled);
                }
                for (String impl : impls) {
                    allResolved.add(impl + "." + methodName);
                }
            } else {
                allResolved.add(baseCalled);
            }
        } else {
            allResolved.add(baseCalled);
        }

        return allResolved;
    }

    private String unwrapConverterMethods(String arg) {
        if (arg == null) return null;
        String current = arg;
        while (current.contains("(") && !current.startsWith("new ")) {
            org.eclipse.jdt.core.dom.ASTNode argNode = parseExpressionString(current);
            if (argNode instanceof org.eclipse.jdt.core.dom.MethodInvocation miArg && !miArg.arguments().isEmpty()) {
                boolean shouldUnpack = false;
                org.eclipse.jdt.core.dom.Expression recv = miArg.getExpression();
                if (recv == null || recv instanceof org.eclipse.jdt.core.dom.ThisExpression) {
                    shouldUnpack = true;
                } else if (recv instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
                    shouldUnpack = !Character.isLowerCase(sn.getIdentifier().charAt(0));
                } else if (recv instanceof org.eclipse.jdt.core.dom.QualifiedName qn) {
                    shouldUnpack = !Character.isLowerCase(qn.getName().getIdentifier().charAt(0));
                }
                if (shouldUnpack) {
                    org.eclipse.jdt.core.dom.Expression innerArg = (org.eclipse.jdt.core.dom.Expression) miArg.arguments().get(0);
                    if (miArg.getName().getIdentifier().equals("valueOf") && miArg.arguments().size() == 2) {
                        innerArg = (org.eclipse.jdt.core.dom.Expression) miArg.arguments().get(1);
                    }
                    current = innerArg.toString();
                    continue;
                }
            }
            break;
        }
        return current;
    }
}