package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.index.AccessorNaming;
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
import org.eclipse.jdt.core.dom.*;
import java.util.*;

public class VariableTracer {
    private final CodebaseContext context;
    private final ConstantResolver constantResolver;
    private ConstantExtractor constantExtractor;
    private TypeResolver typeResolver;

    private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
    private final Map<String, String> variableDeclaredTypeCache = new HashMap<>();
    private final FieldInitializerFinder fieldInitializerFinder;

    public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
        this.context = context;
        this.constantResolver = constantResolver;
        this.dataFlowModel = new click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel(context);
        this.fieldInitializerFinder = new FieldInitializerFinder(context);
    }

    public void setConstantExtractor(ConstantExtractor constantExtractor) {
        this.constantExtractor = constantExtractor;
    }

    public void setTypeResolver(TypeResolver typeResolver) {
        this.typeResolver = typeResolver;
    }

    public void clearAnalysisCaches() {
        variableDeclaredTypeCache.clear();
        if (dataFlowModel instanceof JdtDataFlowModel jdtDataFlowModel) {
            jdtDataFlowModel.clearAnalysisCaches();
        }
    }

    public Expression traceVariable(Expression expr) {
        List<Expression> all = traceVariableAll(expr);
        return all.isEmpty() ? expr : all.get(all.size() - 1);
    }

    private Expression peelExpression(Expression expr) {
        while (expr instanceof ParenthesizedExpression pe) {
            expr = pe.getExpression();
        }
        if (expr instanceof CastExpression ce) {
            return peelExpression(ce.getExpression());
        }
        return expr;
    }

    public List<Expression> traceVariableAll(Expression expr) {
        return dataFlowModel.getReachingDefinitions(expr);
    }

    public List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
        return dataFlowModel.getReachingDefinitions(expr);
    }

    public Expression traceLocalSetter(String methodFqn, String varName, String getterName) {
        if (methodFqn == null || !methodFqn.contains(".")) return null;
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td == null) {
            return null;
        }
        MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
        if (md == null || md.getBody() == null) {
            return null;
        }

        String setterMethodName;
        String indexedFieldName;
        if (AccessorNaming.isBeanStyleAccessorName(getterName)) {
            String propName = AccessorNaming.propertyNameFromAccessor(getterName);
            setterMethodName = "set" + AccessorNaming.capitalizeProperty(propName);
            indexedFieldName = propName;

            String varTypeName = getVariableDeclaredType(methodFqn, varName);
            if (varTypeName != null) {
                CompilationUnit contextCu = md.getRoot() instanceof CompilationUnit cu ? cu : null;
                TypeDeclaration varType = contextCu != null
                        ? context.getTypeDeclaration(varTypeName, contextCu)
                        : null;
                if (varType == null) {
                    varType = context.getTypeDeclaration(varTypeName);
                }
                if (varType != null) {
                    Optional<AccessorSummary> indexedSetter = context.getAccessorIndex()
                            .lookup(context.getFqn(varType), setterMethodName);
                    if (indexedSetter.isPresent() && indexedSetter.get().isSetter()) {
                        setterMethodName = indexedSetter.get().methodName();
                        indexedFieldName = indexedSetter.get().fieldName();
                    }
                }
            }
        } else {
            String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
            setterMethodName = "set" + propName;
            indexedFieldName = propName;
        }

        if (dataFlowModel instanceof JdtDataFlowModel jdtDataFlowModel) {
            return jdtDataFlowModel.findLocalSetterArgument(
                    md, varName, getterName, setterMethodName, indexedFieldName);
        }
        return null;
    }

    private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
        if (td == null) {
            return null;
        }
        String typeFqn = context.getFqn(td);
        if (!visited.add(typeFqn)) {
            return null;
        }

        Optional<String> indexed = context.getFieldTypeIndex().lookup(typeFqn, fieldName);
        if (indexed.isPresent()) {
            return indexed.get();
        }

        return fieldInitializerFinder.resolveFieldTypeName(td, fieldName, visited);
    }

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

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

    public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
        if (methodFqn == null) {
            return null;
        }
        String cacheKey = methodFqn + "|" + cleanFieldName;
        String cached = variableDeclaredTypeCache.get(cacheKey);
        if (cached != null) {
            return cached.isEmpty() ? null : cached;
        }

        String resolved = resolveVariableDeclaredTypeUncached(methodFqn, cleanFieldName);
        variableDeclaredTypeCache.put(cacheKey, resolved == null ? "" : resolved);
        return resolved;
    }

    private String resolveVariableDeclaredTypeUncached(String methodFqn, String cleanFieldName) {
        int lastDot = methodFqn.lastIndexOf('.');
        if (lastDot > 0) {
            String fqn = methodFqn.substring(0, lastDot);
            String methodName = methodFqn.substring(lastDot + 1);
            TypeDeclaration td = context.getTypeDeclaration(fqn);
            if (td != null) {
                MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
                if (md != null) {
                    for (Object pObj : md.parameters()) {
                        SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
                        if (svd.getName().getIdentifier().equals(cleanFieldName)) {
                            return svd.getType().toString();
                        }
                    }
                    final String[] foundType = new String[1];
                    if (md.getBody() != 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(cleanFieldName)) {
                                    foundType[0] = node.getType().toString();
                                }
                            }
                            return super.visit(node);
                        }
                        
                        @Override
                        public boolean visit(LambdaExpression node) {
                            for (Object paramObj : node.parameters()) {
                                VariableDeclaration param = (VariableDeclaration) paramObj;
                                if (param.getName().getIdentifier().equals(cleanFieldName)) {
                                    if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
                                        foundType[0] = svd.getType().toString();
                                        return false;
                                    }
                                    
                                    ASTNode parent = node.getParent();
                                    if (parent instanceof MethodInvocation mi) {
                                        Expression caller = mi.getExpression();
                                        
                                        boolean hasMap = false;
                                        while (caller instanceof MethodInvocation chainMi) {
                                            String mName = chainMi.getName().getIdentifier();
                                            if (mName.equals("map") || mName.equals("flatMap")) {
                                                hasMap = true;
                                                if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof LambdaExpression lambda) {
                                                    if (lambda.getBody() instanceof MethodInvocation mapMi) {
                                                        Expression mapCaller = mapMi.getExpression();
                                                        if (mapCaller instanceof SimpleName mapSn) {
                                                            String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier());
                                                            if (mapCallerType != null) {
                                                                TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
                                                                CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
                                                                TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu);
                                                                if (td != null) {
                                                                    MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true);
                                                                    if (mapMd != null && mapMd.getReturnType2() != null) {
                                                                        foundType[0] = mapMd.getReturnType2().toString();
                                                                        return false;
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                            if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") || 
                                                mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") || 
                                                mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) {
                                                caller = chainMi.getExpression();
                                            } else {
                                                break;
                                            }
                                        }
                                        
                                        if (!hasMap) {
                                            if (caller instanceof SimpleName callerSn) {
                                                String callerName = callerSn.getIdentifier();
                                                if (!callerName.equals(cleanFieldName)) {
                                                    String callerType = getVariableDeclaredType(methodFqn, callerName);
                                                    if (callerType != null && callerType.contains("<")) {
                                                        int start = callerType.indexOf('<');
                                                        int end = callerType.lastIndexOf('>');
                                                        if (start >= 0 && end > start) {
                                                            foundType[0] = callerType.substring(start + 1, end);
                                                            return false;
                                                        }
                                                    }
                                                }
                                            } else if (caller instanceof FieldAccess fa) {
                                                String callerName = fa.getName().getIdentifier();
                                                if (!callerName.equals(cleanFieldName)) {
                                                    String callerType = getVariableDeclaredType(methodFqn, callerName);
                                                    if (callerType != null && callerType.contains("<")) {
                                                        int start = callerType.indexOf('<');
                                                        int end = callerType.lastIndexOf('>');
                                                        if (start >= 0 && end > start) {
                                                            foundType[0] = callerType.substring(start + 1, end);
                                                            return false;
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            return super.visit(node);
                        }
                    });
                }
                if (foundType[0] != null) return foundType[0];
            }
            
            // Fallback to fields including superclasses
            String fieldType = getFieldType(td, cleanFieldName, context, new java.util.HashSet<>());
            if (fieldType != null) return fieldType;
        }
        }
        return null;
    }

    public String traceLocalVariable(String methodFqn, String varName) {
        return traceLocalVariable(methodFqn, varName, null);
    }

    public String traceLocalVariable(String methodFqn, String varName, Map<String, String> branchBindings) {
        String result = traceLocalVariableInternal(methodFqn, varName, branchBindings);
        if (result != null && branchBindings != null && !branchBindings.isEmpty()) {
            String switchEvaluated = evaluateSwitchAssignment(methodFqn, varName, branchBindings);
            if (switchEvaluated != null) {
                return switchEvaluated;
            }
        }
        return result;
    }

    private String traceLocalVariableInternal(String methodFqn, String varName, Map<String, String> branchBindings) {
        if (methodFqn == null || !methodFqn.contains(".")) return null;
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td != null) {
            MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
            if (md != null && md.getBody() != null) {
                List<BranchAssignment> assignments = new ArrayList<>();
                md.getBody().accept(new ASTVisitor() {
                    @Override
                    public boolean visit(VariableDeclarationFragment node) {
                        if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
                            assignments.add(new BranchAssignment(
                                    peelExpression(node.getInitializer()),
                                    click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node)));
                        }
                        return super.visit(node);
                    }
                    @Override
                    public boolean visit(Assignment node) {
                        if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
                            assignments.add(new BranchAssignment(
                                    peelExpression(node.getRightHandSide()),
                                    click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node)));
                        }
                        return super.visit(node);
                    }
                });

                List<BranchAssignment> filtered = filterAssignmentsByBindings(assignments, branchBindings);
                if (!filtered.isEmpty()) {
                    List<String> stringified = new ArrayList<>();
                    for (BranchAssignment assignment : filtered) {
                        Expression expr = assignment.expression();
                        // Under path bindings, resolve MI initializers via CR (mapper.fromString(commandKey)).
                        if (branchBindings != null && !branchBindings.isEmpty()
                                && expr instanceof MethodInvocation mi) {
                            String fromCall = resolveMethodInvocationUnderBindings(mi, methodFqn, branchBindings);
                            if (fromCall != null && !fromCall.isBlank()
                                    && !AstUtils.isAnalysisToken(fromCall)) {
                                stringified.add(fromCall);
                                continue;
                            }
                        }
                        String multiConst = stringifyMultiConstantDefs(expr, branchBindings);
                        if (multiConst != null) {
                            stringified.add(multiConst);
                            continue;
                        }
                        Expression traced = traceVariable(expr);
                        if (traced instanceof MethodInvocation mi) {
                            Expression innerMost = unwrapMethodInvocation(mi, 0, methodFqn);
                            stringified.add(innerMost.toString());
                        } else if (traced instanceof ArrayInitializer) {
                            stringified.add("new Object[]" + traced.toString());
                        } else {
                            stringified.add(traced.toString());
                        }
                    }
                    if (stringified.size() == 1) {
                        return stringified.get(0).replaceAll("->\\s*yield\\s+", "-> ");
                    }

                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < stringified.size() - 1; i++) {
                        sb.append("true ? ").append(stringified.get(i)).append(" : ");
                    }
                    sb.append(stringified.get(stringified.size() - 1));
                    return sb.toString().replaceAll("->\\s*yield\\s+", "-> ");
                }
            }
            
            // If local variable not found, check field assignments
            String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName;
            final Expression[] fieldInitializer = new Expression[1];
            ASTVisitor fieldVisitor = new ASTVisitor() {
                @Override
                public boolean visit(VariableDeclarationFragment node) {
                    if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) {
                        fieldInitializer[0] = node.getInitializer();
                    }
                    return super.visit(node);
                }
                @Override
                public boolean visit(Assignment node) {
                    Expression left = node.getLeftHandSide();
                    if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) ||
                        (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) {
                        fieldInitializer[0] = node.getRightHandSide();
                    }
                    return super.visit(node);
                }
            };
            
            for (FieldDeclaration fd : td.getFields()) {
                for (Object fragObj : fd.fragments()) {
                    VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                    if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) {
                        fieldInitializer[0] = frag.getInitializer();
                    }
                }
            }
            if (fieldInitializer[0] != null) return fieldInitializer[0].toString();

            for (MethodDeclaration classMd : td.getMethods()) {
                if (classMd.isConstructor() && classMd.getBody() != null) {
                    classMd.getBody().accept(fieldVisitor);
                }
            }
            if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
            
            for (MethodDeclaration classMd : td.getMethods()) {
                if (!classMd.isConstructor() && classMd.getBody() != null) {
                    classMd.getBody().accept(fieldVisitor);
                }
            }
            if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
        }
        return null;
    }

    private String resolveMethodInvocationUnderBindings(
            MethodInvocation mi, String callerFqn, Map<String, String> branchBindings) {
        String ownerFqn = resolveInvocationOwnerFqn(mi, callerFqn);
        if (ownerFqn == null) {
            return null;
        }
        TypeDeclaration owner = context.getTypeDeclaration(ownerFqn);
        if (owner == null) {
            return null;
        }
        MethodDeclaration md = context.findMethodDeclaration(owner, mi.getName().getIdentifier(), true);
        if (md == null) {
            return null;
        }
        Map<String, String> callBindings = new HashMap<>(branchBindings != null ? branchBindings : Map.of());
        for (int i = 0; i < md.parameters().size() && i < mi.arguments().size(); i++) {
            SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
            Expression arg = (Expression) mi.arguments().get(i);
            String argValue = null;
            if (arg instanceof SimpleName sn) {
                argValue = callBindings.getOrDefault(sn.getIdentifier(), sn.getIdentifier());
            } else if (arg instanceof StringLiteral sl) {
                argValue = sl.getLiteralValue();
            } else {
                argValue = constantResolver.resolve(arg, context);
            }
            if (argValue != null) {
                callBindings.put(param.getName().getIdentifier(), argValue);
            }
        }
        return constantResolver.evaluateMethodReturn(md, callBindings, context);
    }

    private String resolveInvocationOwnerFqn(MethodInvocation mi, String callerFqn) {
        IMethodBinding methodBinding = mi.resolveMethodBinding();
        if (methodBinding != null && methodBinding.getDeclaringClass() != null
                && !methodBinding.getDeclaringClass().isRecovered()) {
            String fqn = methodBinding.getDeclaringClass().getErasure().getQualifiedName();
            if (fqn != null && fqn.contains(".") && !fqn.startsWith("<")) {
                return fqn;
            }
        }
        Expression receiver = mi.getExpression();
        if (!(receiver instanceof SimpleName sn) || callerFqn == null || !callerFqn.contains(".")) {
            return null;
        }
        String className = callerFqn.substring(0, callerFqn.lastIndexOf('.'));
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td == null) {
            return null;
        }
        CompilationUnit cu = td.getRoot() instanceof CompilationUnit c ? c : null;
        for (FieldDeclaration field : td.getFields()) {
            for (Object fragmentObj : field.fragments()) {
                VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObj;
                if (!fragment.getName().getIdentifier().equals(sn.getIdentifier())) {
                    continue;
                }
                IVariableBinding binding = fragment.resolveBinding();
                if (binding != null && binding.getType() != null && !binding.getType().isRecovered()) {
                    String fqn = binding.getType().getErasure().getQualifiedName();
                    if (fqn != null && fqn.contains(".") && !fqn.startsWith("<")) {
                        return fqn;
                    }
                }
                if (typeResolver != null) {
                    String viaTr = typeResolver.resolveTypeToFqn(field.getType(), cu);
                    if (viaTr != null && viaTr.contains(".")) {
                        return viaTr;
                    }
                }
                AbstractTypeDeclaration resolved =
                        context.getAbstractTypeDeclaration(field.getType().toString(), cu);
                if (resolved != null) {
                    return context.getFqn(resolved);
                }
            }
        }
        return null;
    }

    private String stringifyMultiConstantDefs(Expression expr, Map<String, String> branchBindings) {
        if (expr instanceof SwitchExpression se) {
            Map<String, String> params = branchBindings != null ? branchBindings : Map.of();
            String evaluated = constantResolver.evaluateSwitchWithParams(se, params, context);
            if (evaluated != null) {
                return evaluated;
            }
        }
        boolean pushedBindings = branchBindings != null && !branchBindings.isEmpty();
        if (pushedBindings) {
            context.setAnalysisNamedBindings(branchBindings);
        }
        try {
            List<Expression> defs = traceVariableAll(expr);
            if (defs.size() <= 1) {
                return null;
            }
            LinkedHashSet<String> consts = new LinkedHashSet<>();
            for (Expression def : defs) {
                String val = constantResolver.resolve(def, context);
                if (val == null || val.isBlank() || AstUtils.isAnalysisToken(val)) {
                    return null;
                }
                consts.add(val);
            }
            if (consts.isEmpty()) {
                return null;
            }
            if (consts.size() == 1) {
                return consts.iterator().next();
            }
            return AstUtils.formatEnumSet(consts);
        } finally {
            if (pushedBindings) {
                context.clearAnalysisNamedBindings();
            }
        }
    }

    private String evaluateSwitchAssignment(String methodFqn, String varName, Map<String, String> parameterValues) {
        TypeDeclaration td = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
        if (td == null) {
            return null;
        }
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
        if (md == null || md.getBody() == null) {
            return null;
        }
        final ASTNode[] switchNode = new ASTNode[1];
        md.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(VariableDeclarationFragment node) {
                if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
                    Expression init = peelExpression(node.getInitializer());
                    if (init instanceof SwitchExpression) {
                        switchNode[0] = init;
                        return false;
                    }
                }
                return super.visit(node);
            }

            @Override
            public boolean visit(Assignment node) {
                if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
                    Expression rhs = peelExpression(node.getRightHandSide());
                    if (rhs instanceof SwitchExpression) {
                        switchNode[0] = rhs;
                        return false;
                    }
                }
                return super.visit(node);
            }
        });
        if (switchNode[0] instanceof SwitchExpression se) {
            return constantResolver.evaluateSwitchWithParams(se, parameterValues, context);
        }
        return null;
    }

    private static List<BranchAssignment> filterAssignmentsByBindings(
            List<BranchAssignment> assignments, Map<String, String> branchBindings) {
        if (assignments.isEmpty()) {
            return assignments;
        }
        if (branchBindings == null || branchBindings.isEmpty()) {
            return assignments;
        }
        List<BranchAssignment> compatible = new ArrayList<>();
        List<BranchAssignment> unconstrained = new ArrayList<>();
        for (BranchAssignment assignment : assignments) {
            String constraint = assignment.constraint();
            if (constraint == null || constraint.isBlank()) {
                unconstrained.add(assignment);
                continue;
            }
            if (BooleanConstraintEvaluator.isCompatibleWithBindings(constraint, branchBindings)) {
                compatible.add(assignment);
            }
        }
        if (!compatible.isEmpty()) {
            return compatible;
        }
        return unconstrained;
    }

    private record BranchAssignment(Expression expression, String constraint) {}

    public Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
        return buildParameterValuesMap(caller, target, callGraph, path, pathIndex, Map.of());
    }

    public Map<String, String> buildParameterValuesMap(
            String caller,
            String target,
            Map<String, List<CallEdge>> callGraph,
            List<String> path,
            int pathIndex,
            Map<String, String> bindings) {
        Map<String, String> paramValues = new HashMap<>();
        context.setAnalysisCallPath(path);
        try {
            return buildParameterValuesMapInternal(caller, target, callGraph, path, pathIndex, bindings, paramValues);
        } finally {
            context.clearAnalysisCallPath();
        }
    }

    private Map<String, String> buildParameterValuesMapInternal(
            String caller,
            String target,
            Map<String, List<CallEdge>> callGraph,
            List<String> path,
            int pathIndex,
            Map<String, String> bindings,
            Map<String, String> paramValues) {
        List<CallEdge> edges = callGraph.get(caller);
        
        boolean hasEdge = false;
        if (edges != null) {
            for (CallEdge edge : edges) {
                if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
                    hasEdge = true;
                    break;
                }
            }
        }

        // Bridge polymorphic interfaces to implementations without graph edges
        if (!hasEdge) {
            String callerSimple = caller.contains(".") ? caller.substring(caller.lastIndexOf('.') + 1) : caller;
            String targetSimple = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
            if (callerSimple.equals(targetSimple)) {
                List<String> callerParams = getParameterNames(caller);
                List<String> targetParams = getParameterNames(target);
                if (callerParams != null && targetParams != null && callerParams.size() == targetParams.size()) {
                    for (int i = 0; i < callerParams.size(); i++) {
                        paramValues.put(targetParams.get(i), callerParams.get(i));
                    }
                    return paramValues;
                }
            }
        }

        if (edges == null) {
            return paramValues;
        }

        CallEdge selectedEdge = CallSiteMatcher.selectEdge(caller, target, edges, bindings);
        if (selectedEdge == null) {
            return paramValues;
        }

        List<String> args = selectedEdge.getArguments();
        if (args == null || args.isEmpty()) {
            return paramValues;
        }

        int lastDot = target.lastIndexOf('.');
        if (lastDot < 0) {
            return paramValues;
        }
        String className = target.substring(0, lastDot);
        String methodName = target.substring(lastDot + 1);

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

        MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
        if (md == null) {
            return paramValues;
        }

        List<String> paramNames = new ArrayList<>();
        for (Object paramObj : md.parameters()) {
            SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
            paramNames.add(param.getName().getIdentifier());
        }

        int minSize = Math.min(paramNames.size(), args.size());
        for (int j = 0; j < minSize; j++) {
            String argValue = args.get(j);
            String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
            if (typeResolver != null
                    && FunctionalInterfaceTypes.isFunctionalInterface(typeResolver.getParameterType(target, j))) {
                List<String> constants = resolveConstantsFromCallSiteArgument(caller, target, j, selectedEdge);
                if (constants.size() == 1) {
                    resolvedArgValue = constants.get(0);
                }
            }
            paramValues.put(paramNames.get(j), resolvedArgValue);
        }
        return paramValues;
    }

    private List<String> getParameterNames(String methodFqn) {
        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, true);
        if (md == null) return null;

        List<String> paramNames = new ArrayList<>();
        for (Object paramObj : md.parameters()) {
            SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
            paramNames.add(param.getName().getIdentifier());
        }
        return paramNames;
    }

    public String resolveArgumentValue(String argValue, String callerMethod, List<String> path, int pathIndex, Map<String, List<CallEdge>> callGraph) {
        if (argValue == null) return null;
        if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*") || (argValue.startsWith("\"") && argValue.endsWith("\""))) {
            return argValue;
        }

        String tracedLocal = traceLocalVariable(callerMethod, argValue);
        if (tracedLocal != null && !tracedLocal.equals(argValue)) {
            if (tracedLocal.contains(".") || tracedLocal.startsWith("new ") || tracedLocal.matches(".*[0-9].*") || tracedLocal.contains("(") || (tracedLocal.startsWith("\"") && tracedLocal.endsWith("\""))) {
                return tracedLocal;
            }
            argValue = tracedLocal;
        }

        int callerIndex = path.indexOf(callerMethod);
        if (callerIndex <= 0) return argValue;

        String prevCaller = path.get(callerIndex - 1);
        List<CallEdge> prevEdges = callGraph.get(prevCaller);
        if (prevEdges == null) return argValue;

        for (CallEdge edge : prevEdges) {
            if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) {
                List<String> prevArgs = edge.getArguments();
                if (prevArgs == null || prevArgs.isEmpty()) break;

                int lastDot = callerMethod.lastIndexOf('.');
                if (lastDot < 0) break;
                String className = callerMethod.substring(0, lastDot);
                String methodName = callerMethod.substring(lastDot + 1);

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

                MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
                if (md == null) break;

                List<String> paramNames = new ArrayList<>();
                for (Object paramObj : md.parameters()) {
                    SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
                    paramNames.add(param.getName().getIdentifier());
                }

                int paramIdx = paramNames.indexOf(argValue);
                if (paramIdx >= 0 && paramIdx < prevArgs.size()) {
                    String prevArg = prevArgs.get(paramIdx);
                    return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph);
                }
                break;
            }
        }
        return argValue;
    }

    private boolean isHeuristicMatch(String neighbor, String target) {
        if (neighbor == null || target == null) return false;
        if (neighbor.equals(target)) return true;

        if (neighbor.contains(".") && target.contains(".")) {
            String methodNeighbor = neighbor.substring(neighbor.lastIndexOf('.') + 1);
            String methodTarget = target.substring(target.lastIndexOf('.') + 1);
            if (!methodNeighbor.equals(methodTarget)) return false;

            String classNeighbor = neighbor.substring(0, neighbor.lastIndexOf('.'));
            String classTarget = target.substring(0, target.lastIndexOf('.'));
            if (classNeighbor.equals(classTarget)) return true;

            if (context.areSameTypeOrUnambiguousSimpleMatch(classNeighbor, classTarget)) return true;

            List<String> impls = context.getImplementations(classTarget);
            if (impls != null) {
                for (String impl : impls) {
                    if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true;
                }
            }

            List<String> implsN = context.getImplementations(classNeighbor);
            if (implsN != null) {
                for (String impl : implsN) {
                    if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true;
                }
            }
            return false;
        }

        String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
        String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
        if (!simpleNeighbor.equals(simpleTarget)) return false;
        
        String classNeighbor = neighbor.contains(".") ? neighbor.substring(0, neighbor.lastIndexOf('.')) : null;
        String classTarget = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : null;
        String simpleClassTarget = classTarget != null && classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
        String simpleClassNeighbor = classNeighbor != null && classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;

        if (simpleClassNeighbor != null && simpleClassTarget != null) {
            String leftClass = classNeighbor != null ? classNeighbor : simpleClassNeighbor;
            String rightClass = classTarget != null ? classTarget : simpleClassTarget;
            if (context.areSameTypeOrUnambiguousSimpleMatch(leftClass, rightClass)) return true;
            if (!context.isAmbiguousSimpleName(simpleClassNeighbor)
                    && !context.isAmbiguousSimpleName(simpleClassTarget)
                    && simpleClassNeighbor.equalsIgnoreCase(simpleClassTarget)) {
                return true;
            }
            if (simpleClassNeighbor.equals("this") || simpleClassNeighbor.equals("super")) return true;
            return false;
        }
        
        return true;
    }

    private Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
        return MethodInvocationUnwrapper.unwrap(mi, depth, methodFqn, context, miArg -> resolveTargetMethodFqn(miArg, methodFqn));
    }
    
    private String resolveTargetMethodFqn(MethodInvocation mi, String currentMethodFqn) {
        if (currentMethodFqn == null || !currentMethodFqn.contains(".")) return null;
        String currentClass = currentMethodFqn.substring(0, currentMethodFqn.lastIndexOf('.'));
        if (mi.getExpression() == null || mi.getExpression().toString().equals("this")) {
            return currentClass + "." + mi.getName().getIdentifier();
        }
        if (mi.getExpression() instanceof SimpleName sn) {
            String declaredType = getVariableDeclaredType(currentMethodFqn, sn.getIdentifier());
            if (declaredType != null) {
                return declaredType + "." + mi.getName().getIdentifier();
            }
        } else if (mi.getExpression() instanceof FieldAccess fa) {
            String declaredType = getVariableDeclaredType(currentMethodFqn, fa.getName().getIdentifier());
            if (declaredType != null) {
                return declaredType + "." + mi.getName().getIdentifier();
            }
        }
        return null;
    }

    /**
     * Resolves enum/constants passed at a call site using source AST + {@link JdtDataFlowModel}.
     * Used for {@code Supplier}, {@code Runnable}, and similar functional parameters.
     */
    public List<String> resolveConstantsFromCallSiteArgument(String caller, String callee, int paramIndex) {
        return resolveConstantsFromCallSiteArgument(caller, callee, paramIndex, null);
    }

    public List<String> resolveConstantsFromCallSiteArgument(
            String caller, String callee, int paramIndex, CallEdge edge) {
        if (constantExtractor == null) {
            return List.of();
        }
        List<String> constants = new ArrayList<>();
        Expression sourceArg = CallSiteMatcher.findCallSiteArgumentExpression(
                caller, callee, paramIndex, edge, context);
        if (sourceArg != null) {
            resolveConstantsFromExpressionViaDataflow(sourceArg, constants);
        }
        return constants;
    }

    public int findFunctionalParameterIndex(String methodFqn) {
        if (methodFqn == null || typeResolver == null) {
            return -1;
        }
        for (int i = 0; i < 8; i++) {
            String paramType = typeResolver.getParameterType(methodFqn, i);
            if (paramType == null) {
                break;
            }
            if (FunctionalInterfaceTypes.isFunctionalInterface(paramType)) {
                return i;
            }
        }
        return -1;
    }

    public static boolean isFunctionalSamMethod(String methodFqn) {
        return FunctionalInterfaceTypes.isFunctionalSamMethod(methodFqn);
    }

    public void resolveConstantsViaDataflow(Expression expression, List<String> constants) {
        resolveConstantsFromExpressionViaDataflow(expression, constants);
    }

    public List<String> resolveConstantsFromSourceText(
            String expressionText, List<String> searchMethods, List<String> callPath) {
        if (expressionText == null || expressionText.isBlank()) {
            return List.of();
        }
        context.setAnalysisCallPath(callPath);
        try {
            Expression anchored = AstUtils.findExpressionInMethods(searchMethods, expressionText, context);
            Expression expr = anchored != null ? anchored : AstUtils.parseExpression(expressionText);
            if (expr == null) {
                return List.of();
            }
            List<String> constants = new ArrayList<>();
            resolveConstantsViaDataflow(expr, constants);
            return constants.stream()
                    .filter(c -> c != null && !c.isBlank())
                    .distinct()
                    .toList();
        } finally {
            context.clearAnalysisCallPath();
        }
    }

    private void resolveConstantsFromExpressionViaDataflow(Expression expression, List<String> constants) {
        for (Expression def : dataFlowModel.getReachingDefinitions(expression)) {
            constantExtractor.extractConstantsFromExpression(def, constants);
        }
        if (constants.isEmpty()) {
            constantExtractor.extractConstantsFromExpression(expression, constants);
        }
    }

    Expression findCallSiteArgumentExpression(String caller, String callee, int paramIndex) {
        return CallSiteMatcher.findCallSiteArgumentExpression(caller, callee, paramIndex, null, context);
    }
}
