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 class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
    protected String currentMethodFqn;
    protected Map<String, List<CallEdge>> graph;

    public HeuristicCallGraphEngine(CodebaseContext context) {
        super(context);
    }

    @SuppressWarnings("unchecked")
    protected Map<String, List<CallEdge>> buildCallGraph() {
        Map<String, List<CallEdge>> cached = (Map<String, List<CallEdge>>) context.getCache().get("heuristicCallGraph");
        if (cached != null) {
            this.graph = cached;
            return cached;
        }
        graph = new HashMap<>();
        for (CompilationUnit cu : context.getCompilationUnits()) {
            cu.accept(new ASTVisitor() {

                @Override
                public boolean visit(MethodInvocation node) {
                    MethodDeclaration md = findEnclosingMethod(node);
                    if (md != null) {
                        TypeDeclaration td = findEnclosingType(md);
                        if (td != null) {
                            String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
                            List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
                            List<String> args = resolveArguments(node.arguments());
                            String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
                            for (String calledMethod : calledMethods) {
                                String receiver = getReceiverString(node.getExpression());
                                CallEdge edge = new CallEdge(calledMethod, args, receiver);
                                edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
                                graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
                            }
                            for (Object argObj : node.arguments()) {
                                if (argObj instanceof ExpressionMethodReference emr) {
                                    String typeName = emr.getExpression().toString();
                                    if ("this".equals(typeName) || "super".equals(typeName)) {
                                        TypeDeclaration td2 = findEnclosingType(node);
                                        if (td2 != null) {
                                            String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
                                            if (refMethod != null) {
                                                List<String> implicitArgs = new ArrayList<>();
                                                if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
                                                    implicitArgs.add(node.getExpression().toString());
                                                } else {
                                                    implicitArgs.addAll(args);
                                                }
                                                String receiver = getReceiverString(node.getExpression());
                                                CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
                                                edge.setConstraint(constraint);
                                                graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
                                            }
                                        }
                                    } else {
                                        String fallbackTypeFqn = null;
                                        ITypeBinding binding = emr.getExpression().resolveTypeBinding();
                                        if (binding != null) {
                                            fallbackTypeFqn = binding.getQualifiedName();
                                        } else if (emr.getExpression() instanceof SimpleName sn) {
                                            fallbackTypeFqn = resolveReceiverTypeFallback(sn);
                                        }
                                        if (fallbackTypeFqn != null) {
                                            String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
                                            List<String> implicitArgs = new ArrayList<>();
                                            if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
                                                implicitArgs.add(node.getExpression().toString());
                                            } else {
                                                implicitArgs.addAll(args);
                                            }
                                            String receiver = getReceiverString(node.getExpression());
                                            CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
                                            edge.setConstraint(constraint);
                                            graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
                                            
                                            List<String> impls = context.getImplementations(fallbackTypeFqn);
                                            for (String impl : impls) {
                                                CallEdge implEdge = new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver);
                                                implEdge.setConstraint(constraint);
                                                graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    return super.visit(node);
                }

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

    @Override
    protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
        // Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
        // where we can substitute the actual receiver from the call edge
        boolean isThisOrSuperGetter = (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi
                && (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression || mi.getExpression() instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation)
                && mi.arguments().isEmpty()) || (originalExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi && smi.arguments().isEmpty());

        if (isThisOrSuperGetter) {
            return originalExpr.toString(); // Preserve "this.getTransitionType()" or "super.getEvent()" for later substitution
        }

        // Narrow resolution for "localVar.getX()" patterns where localVar is initialized
        // from a ClassInstanceCreation (directly or via a factory method). This avoids
        // the broad ENUM_SET fallback when the getter transforms one enum to another.
        if (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation getterMi
                && getterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName instanceSn
                && getterMi.arguments().isEmpty()) {
            String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
            if (narrowed != null) {
                return narrowed;
            }
        }

        return resolvedValue;
    }
    private String findDefiningClass(String className, String methodName) {
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td == null) return null;
        
        String resolvedMethodFqn = resolveMethodInTypeHierarchy(td, methodName);
        if (resolvedMethodFqn != null) {
            return resolvedMethodFqn.substring(0, resolvedMethodFqn.lastIndexOf('.'));
        }
        return null;
    }

    protected String resolveCalledMethod(MethodInvocation node) {
        Expression receiver = node.getExpression();
        String methodName = node.getName().getIdentifier();

        if (receiver instanceof MethodInvocation miReceiver) {
            String returnType = resolveMethodInvocationReturnType(miReceiver);
            if (returnType != null) {
                return returnType + "." + methodName;
            }
        }

        if (receiver == null) {
            TypeDeclaration td = findEnclosingType(node);
            if (td != null) {
                return resolveMethodInTypeHierarchy(td, methodName);
            }
            return null;
        }

        ITypeBinding binding = receiver.resolveTypeBinding();
        if (binding != null) {
            return binding.getQualifiedName() + "." + methodName;
        }

        if (receiver instanceof ThisExpression) {
            TypeDeclaration td = findEnclosingType(node);
            if (td != null) {
                return context.getFqn(td) + "." + methodName;
            }
        }

        if (receiver instanceof SimpleName sn) {
            String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
            if (fallbackTypeFqn != null) {
                return fallbackTypeFqn + "." + methodName;
            }
            String receiverName = sn.getIdentifier();
            return receiverName + "." + methodName; 
        }

        if (receiver instanceof QualifiedName qn) {
            String fqn = qn.getFullyQualifiedName();
            TypeDeclaration td = context.getTypeDeclaration(fqn);
            if (td != null) {
                return fqn + "." + methodName;
            }
            return fqn + "." + methodName; 
        }

        return null;
    }

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

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

        // 2. Check fields in enclosing class and its superclasses
        TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
        TypeDeclaration currentType = enclosingType;
        while (currentType != null) {
            for (FieldDeclaration field : currentType.getFields()) {
                for (Object fragObj : field.fragments()) {
                    VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                    if (frag.getName().getIdentifier().equals(varName)) {
                        return resolveTypeToFqn(field.getType(), receiverNameNode);
                    }
                }
            }
            String superclass = context.getSuperclassFqn(currentType);
            currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
        }

        return null;
    }

    protected String resolveTypeToFqn(Type type, ASTNode contextNode) {
        if (type == null) return null;
        String simpleName;
        if (type.isSimpleType()) {
            simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
        } else if (type.isParameterizedType()) {
            return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
        } else {
            simpleName = type.toString();
        }

        CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
        TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
        if (td != null) {
            return context.getFqn(td);
        }
        
        // Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
        for (Object impObj : cu.imports()) {
            ImportDeclaration imp = (ImportDeclaration) impObj;
            String impName = imp.getName().getFullyQualifiedName();
            if (impName.endsWith("." + simpleName)) {
                return impName;
            }
        }

        return simpleName;
    }

    /**
     * Attempts to narrow the resolution of {@code instanceVar.getterMethod()} when the
     * instance variable is initialised from a {@link org.eclipse.jdt.core.dom.ClassInstanceCreation}
     * (either directly or via a factory/mapper method).
     *
     * <p>This handles both:
     * <ul>
     *   <li>Simple pass-through: {@code Wrapper(MyEnum e){this.e=e;} getE(){return e;}}</li>
     *   <li>Transformation: {@code getSmEvent(){return switch(dtoEnum){case A->SmEvents.X;…};}}</li>
     * </ul>
     *
     * @return the resolved constant string, or {@code null} when narrowing is not possible
     */
    private String tryNarrowGetterViaLocalVarChain(
            MethodInvocation getterCall, SimpleName instanceSn) {

        MethodDeclaration enclosing = findEnclosingMethod(getterCall);
        if (enclosing == null || enclosing.getBody() == null) return null;

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

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

        // Obtain the ClassInstanceCreation that created the object
        org.eclipse.jdt.core.dom.ClassInstanceCreation cic = null;
        String receiverType = null;
        if (initExpr[0] instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation directCic) {
            cic = directCic;
        } else if (initExpr[0] instanceof MethodInvocation factoryMi
                && (factoryMi.getExpression() instanceof SimpleName || factoryMi.getExpression() instanceof QualifiedName)) {
            // e.g. "mapper.toMsg(dto)" or "com.example.Factory.toMsg()"
            if (factoryMi.getExpression() instanceof SimpleName receiverSn) {
                receiverType = resolveReceiverTypeFallback(receiverSn);
            } else if (factoryMi.getExpression() instanceof QualifiedName qn) {
                receiverType = qn.getFullyQualifiedName();
            }
            if (receiverType != null) {
                cic = findReturnedCIC(receiverType, factoryMi.getName().getIdentifier(), context);
            }
        }
        if (cic == null) {
            // Support builder pattern: e.g. EventWrapper.builder().event(TransitionEnum.STATE_X).build()
            if (initExpr[0] instanceof MethodInvocation initMi) {
                String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
                Expression currentMi = initMi;
                while (currentMi instanceof MethodInvocation chainMi) {
                    String mName = chainMi.getName().getIdentifier();
                    if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
                        if (!chainMi.arguments().isEmpty()) {
                            return chainMi.arguments().get(0).toString();
                        }
                    }
                    currentMi = chainMi.getExpression();
                }
            }
            return null;
        }

        // Resolve the variable's declared type to a TypeDeclaration
        CompilationUnit cu = (CompilationUnit) getterCall.getRoot();
        TypeDeclaration varTypeTd = context.getTypeDeclaration(varTypeName[0], cu);
        if (varTypeTd == null) varTypeTd = context.getTypeDeclaration(varTypeName[0]);
        if (varTypeTd == null) return null;

// Evaluate the getter body with field values substituted from the CIC's constructor args
        java.util.Map<String, String> initialLocals = new java.util.HashMap<>();
        if (initExpr[0] instanceof MethodInvocation factoryMi) {
            MethodDeclaration md = context.findMethodDeclaration(context.getTypeDeclaration(receiverType != null ? receiverType : varTypeName[0]), factoryMi.getName().getIdentifier(), true);
            if (md != null) {
                for (int i = 0; i < md.parameters().size() && i < factoryMi.arguments().size(); i++) {
                    SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
                    Expression arg = (Expression) factoryMi.arguments().get(i);
                    String resolvedVal = constantResolver.resolve(arg, context);
                    if (resolvedVal != null) {
                        initialLocals.put(param.getName().getIdentifier(), resolvedVal);
                    }
                }
            }
        }
        
        java.util.Map<String, String> fieldValues = constructorAnalyzer.buildFieldValuesFromCIC(varTypeTd, cic, context, this::evaluateMethodCallInConstructor, initialLocals);
        if (fieldValues.isEmpty()) return null;

        // Track setter calls on the variable between its initialization and the getter call
        // to capture field updates like e.setType("NEW_VALUE")
        trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);

        MethodDeclaration getter = context.findMethodDeclaration(varTypeTd, getterName, true);
        if (getter == null || getter.getBody() == null) return null;

        return constantResolver.evaluateMethodBodyWithLocals(
                getter, fieldValues, context, new java.util.HashSet<>());
    }

    /**
     * Scans the method body for setter calls or field assignments to the given variable
     * that occur between the variable's initialization and the getter call.
     * Updates fieldValues with any field values set via setters.
     */
    private void trackSetterCallsBetween(org.eclipse.jdt.core.dom.Block methodBody, String varName,
            org.eclipse.jdt.core.dom.MethodInvocation getterCall,
            java.util.Map<String, String> fieldValues,
            CodebaseContext context) {
        java.util.List<org.eclipse.jdt.core.dom.ASTNode> statements = new java.util.ArrayList<>();
        for (Object stmtObj : methodBody.statements()) {
            statements.add((org.eclipse.jdt.core.dom.ASTNode) stmtObj);
        }

        int varDeclIndex = -1;
        int getterCallIndex = -1;
        for (int i = 0; i < statements.size(); i++) {
            org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
            // Find variable declaration
            if (varDeclIndex == -1) {
                if (stmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement vds) {
                    for (Object fragObj : vds.fragments()) {
                        if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment frag
                                && frag.getName().getIdentifier().equals(varName)) {
                            varDeclIndex = i;
                            break;
                        }
                    }
                }
            }
            // Find getter call
            if (getterCallIndex == -1) {
                if (stmt.toString().contains(getterCall.toString())) {
                    getterCallIndex = i;
                    break;
                }
            }
        }

        if (varDeclIndex >= 0 && getterCallIndex > varDeclIndex) {
            // Scan statements between var declaration and getter call
            for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
                org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
                if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
                    org.eclipse.jdt.core.dom.Expression expr = es.getExpression();
                    // Check for setter calls: var.setField(value)
                    if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
                        if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
                                && sn.getIdentifier().equals(varName)) {
                            String methodName = mi.getName().getIdentifier();
                            // Check if it's a setter (setXxx or xxx)
                            if (methodName.startsWith("set") && methodName.length() > 3) {
                                String fieldName = Character.toLowerCase(methodName.charAt(3))
                                        + methodName.substring(4);
                                if (!mi.arguments().isEmpty()) {
                                    org.eclipse.jdt.core.dom.Expression arg = (org.eclipse.jdt.core.dom.Expression) mi.arguments().get(0);
                                    String val = constantResolver.resolve(arg, context);
                                    if (val != null) {
                                        fieldValues.put(fieldName, val);
                                        fieldValues.put("this." + fieldName, val);
                                    }
                                }
                            }
                        }
                    }
                    // Check for direct field assignments: var.field = value
                    else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) {
                        if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
                            if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
                                    && sn.getIdentifier().equals(varName)) {
                                String fieldName = fa.getName().getIdentifier();
                                org.eclipse.jdt.core.dom.Expression rhs = assignment.getRightHandSide();
                                String val = constantResolver.resolve(rhs, context);
                                if (val != null) {
                                    fieldValues.put(fieldName, val);
                                    fieldValues.put("this." + fieldName, val);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Extracts a string representation of the receiver expression from a MethodInvocation.
     * Handles ThisExpression, SimpleName, and other expression types.
     */
    protected String getReceiverString(Expression receiver) {
        if (receiver == null) {
            return null;
        }
        if (receiver instanceof ThisExpression) {
            return "this";
        }
        if (receiver instanceof SuperMethodInvocation) {
            return "super";
        }
        if (receiver instanceof SimpleName sn) {
            return sn.getIdentifier();
        }
        return receiver.toString();
    }

    /**
     * Finds the receiver expression used in the caller method when calling the target method.
     * Walks the caller's AST to find a MethodInvocation with the target method name
     * and returns the receiver string.
     */
    protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
        int lastDot = callerFqn.lastIndexOf('.');
        if (lastDot < 0) return null;
        String callerClass = callerFqn.substring(0, lastDot);
        String callerMethod = callerFqn.substring(lastDot + 1);

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

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

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

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

    protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
        Expression receiver = mi.getExpression();
        String receiverType = null;
        if (receiver == null) {
            TypeDeclaration td = findEnclosingType(mi);
            if (td != null) {
                receiverType = context.getFqn(td);
            }
        } else if (receiver instanceof SimpleName sn) {
            receiverType = resolveReceiverTypeFallback(sn);
        } else if (receiver instanceof FieldAccess fa) {
            receiverType = resolveReceiverTypeFallback(fa.getName());
        } else if (receiver instanceof MethodInvocation innerMi) {
            receiverType = resolveMethodInvocationReturnType(innerMi);
        }
        
        if (receiverType == null) {
            ITypeBinding binding = receiver != null ? receiver.resolveTypeBinding() : null;
            if (binding != null) {
                receiverType = binding.getErasure().getQualifiedName();
            }
        }
        
        if (receiverType != null) {
            String rawType = receiverType;
            if (rawType.contains("<")) {
                rawType = rawType.substring(0, rawType.indexOf('<'));
            }
            if (rawType.equals("java.util.Map") || rawType.equals("Map")) {
                String valType = resolveMapValueType(mi);
                if (valType != null) return valType;
            }
            if (receiverType.contains("<")) {
                receiverType = receiverType.substring(0, receiverType.indexOf('<'));
            }
            String methodName = mi.getName().getIdentifier();
            TypeDeclaration td = context.getTypeDeclaration(receiverType);
            if (td == null && receiverType.contains(".")) {
                String simpleClass = receiverType.substring(receiverType.lastIndexOf('.') + 1);
                td = context.getTypeDeclaration(simpleClass);
            }
            if (td != null) {
                MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
                if (md != null && md.getReturnType2() != null) {
                    return resolveTypeToFqn(md.getReturnType2(), mi);
                }
            }
        }
        
        IMethodBinding methodBinding = mi.resolveMethodBinding();
        if (methodBinding != null && methodBinding.getReturnType() != null) {
            return methodBinding.getReturnType().getErasure().getQualifiedName();
        }
        
        return null;
    }
}