package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConstructorAnalyzer {
    private final CodebaseContext context;
    private final ConstantResolver constantResolver;
    private VariableTracer variableTracer;
    private ConstantExtractor constantExtractor;
    private final Map<String, ClassInstanceCreation> returnedCicCache = new HashMap<>();
    private final Set<String> returnedCicAbsent = new HashSet<>();

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

    public ConstructorAnalyzer(CodebaseContext context, ConstantResolver constantResolver) {
        this.context = context;
        this.constantResolver = constantResolver;
    }

    public void setVariableTracer(VariableTracer variableTracer) {
        this.variableTracer = variableTracer;
    }

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

    public void clearReturnedCicCache() {
        returnedCicCache.clear();
        returnedCicAbsent.clear();
    }

    public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
        return traceFieldInConstructors(td, fieldName, context, visited, ResolutionBudget.defaults());
    }

    public List<String> traceFieldInConstructors(
            TypeDeclaration td,
            String fieldName,
            CodebaseContext context,
            Set<String> visited,
            ResolutionBudget budget) {
        if (td == null || fieldName == null) return Collections.emptyList();
        String fqn = context.getFqn(td);
        if (fqn == null) return Collections.emptyList();

        String cycleKey = fqn + "#" + fieldName;
        if (!visited.add(cycleKey)) {
            return Collections.emptyList();
        }

        List<String> results = new ArrayList<>();
        
        for (FieldDeclaration fd : td.getFields()) {
            for (Object fragObj : fd.fragments()) {
                VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
                    Expression right = frag.getInitializer();
                    if (variableTracer != null && constantExtractor != null) {
                        List<Expression> tracedRights = variableTracer.traceVariableAll(right);
                        for (Expression tracedRight : tracedRights) {
                            if (tracedRight instanceof MethodInvocation mi) {
                                String calledName = mi.getName().getIdentifier();
                                String targetClass = context.getFqn(td);
                                CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
                                if (mi.getExpression() == null) {
                                    TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
                                    if (targetTd != null) {
                                        targetClass = context.getFqn(targetTd);
                                    }
                                }
                                results.addAll(constantExtractor.resolveMethodReturnConstant(
                                        targetClass, calledName, 0, visited, cu, budget));
                            } else {
                                String val = constantResolver.resolve(tracedRight, context);
                                if (val != null) results.add(val);
                            }
                        }
                    }
                }
            }
        }

        ASTVisitor assignmentVisitor = new ASTVisitor() {
            @Override
            public boolean visit(Assignment node) {
                Expression left = node.getLeftHandSide();
                if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
                    (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
                    
                    Expression right = node.getRightHandSide();
                    if (variableTracer != null && constantExtractor != null) {
                        List<Expression> tracedRights = variableTracer.traceVariableAll(right);
                        for (Expression tracedRight : tracedRights) {
                            if (tracedRight instanceof MethodInvocation mi) {
                                String calledName = mi.getName().getIdentifier();
                                String targetClass = context.getFqn(td);
                                
                                if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
                                    targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
                                } else if (mi.getExpression() instanceof SimpleName) {
                                    String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
                                    CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
                                    TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
                                    if (targetTd != null) {
                                        targetClass = context.getFqn(targetTd);
                                    }
                                } else if (mi.getExpression() == null) {
                                    CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
                                    TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
                                    if (targetTd != null) {
                                        targetClass = context.getFqn(targetTd);
                                    }
                                }

                                CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
                                results.addAll(constantExtractor.resolveMethodReturnConstant(
                                        targetClass, calledName, 0, visited, cu, budget));
                            } else {
                                String val = constantResolver.resolve(tracedRight, context);
                                if (val != null) results.add(val);
                            }
                        }
                    }
                }
                return super.visit(node);
            }
            @Override
            public boolean visit(ConstructorInvocation node) {
                processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited);
                return super.visit(node);
            }
            
            @Override
            public boolean visit(SuperConstructorInvocation node) {
                TypeDeclaration enclosingTd = findEnclosingType(node);
                if (enclosingTd != null) {
                    String superFqn = context.getSuperclassFqn(enclosingTd);
                    if (superFqn != null) {
                        TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
                        processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited);
                    }
                }
                return super.visit(node);
            }
        };

        for (MethodDeclaration md : td.getMethods()) {
            if (md.isConstructor() && md.getBody() != null) {
                md.getBody().accept(assignmentVisitor);
            }
        }

        for (Object bodyDecl : td.bodyDeclarations()) {
            if (bodyDecl instanceof Initializer init && init.getBody() != null) {
                init.getBody().accept(assignmentVisitor);
            }
        }

        if (results.isEmpty()) {
            // Context-Aware Fallback (Up): Check superclass constructors
            String superFqn = context.getSuperclassFqn(td);
            if (superFqn != null && !superFqn.equals("java.lang.Object")) {
                TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
                if (superTd != null) {
                    results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited, budget.child()));
                }
            }
        }

        if (results.isEmpty()) {
            // Context-Aware Fallback (Down): widen only for interface/abstract types
            if (shouldWidenToImplementations(td)) {
                List<String> impls = context.getImplementations(fqn);
                if (impls != null) {
                    for (String implFqn : impls) {
                        TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
                        if (implTd != null) {
                            results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited, budget.child()));
                        }
                    }
                }
            }
        }

        if (results.isEmpty() && budget != null && budget.allowGlobalFieldScan()) {
            // Global Fallback: Search all parsed classes in the workspace (explicit opt-in only)
            Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
            if (allTypes != null) {
                int matchedCount = 0;
                for (TypeDeclaration typeDecl : allTypes) {
                    if (typeDecl == null) continue;
                    if (context.hasField(typeDecl, fieldName)) {
                        results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited, budget.child()));
                        matchedCount++;
                        if (matchedCount >= budget.globalFieldScanLimit()) {
                            break;
                        }
                    }
                }
            }
        }

        visited.remove(cycleKey);
        return results;
    }

    private static boolean shouldWidenToImplementations(TypeDeclaration td) {
        if (td == null) {
            return true;
        }
        return td.isInterface() || Modifier.isAbstract(td.getModifiers());
    }

    public Map<String, String> buildFieldValuesFromCIC(
            TypeDeclaration td,
            ClassInstanceCreation cic,
            CodebaseContext context,
            RhsResolver rhsResolver,
            Map<String, String> initialLocals) {

        String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
        TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName);
        if (actualTd == null) {
            actualTd = td; 
        }

        Map<String, String> fieldValues = new HashMap<>();
        Set<String> visitedCtors = new HashSet<>();
        buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors, rhsResolver, initialLocals);
        return fieldValues;
    }

    public Map<String, String> buildFieldValuesFromCIC(
            TypeDeclaration td,
            ClassInstanceCreation cic,
            CodebaseContext context,
            RhsResolver rhsResolver) {
        return buildFieldValuesFromCIC(td, cic, context, rhsResolver, null);
    }

    public Map<String, String> buildFieldValuesFromCIC(
            TypeDeclaration td,
            ClassInstanceCreation cic,
            CodebaseContext context) {
        return buildFieldValuesFromCIC(td, cic, context, null, null);
    }

    private void buildFieldValuesFromCICRecursive(
            TypeDeclaration td,
            ClassInstanceCreation cic,
            CodebaseContext context,
            Map<String, String> fieldValues,
            Set<String> visitedCtors,
            RhsResolver rhsResolver,
            Map<String, String> initialLocals) {

        String tdKey = context.getFqn(td);
        if (!visitedCtors.add(tdKey)) return;

        IMethodBinding resolvedCtor = cic.resolveConstructorBinding();

        for (MethodDeclaration ctor : td.getMethods()) {
            if (!ctor.isConstructor() || ctor.getBody() == null) continue;
            
            boolean matches = false;
            if (resolvedCtor != null && ctor.resolveBinding() != null) {
                matches = resolvedCtor.isEqualTo(ctor.resolveBinding()) || resolvedCtor.getKey().equals(ctor.resolveBinding().getKey());
            }
            if (!matches) {
                matches = ctor.parameters().size() == cic.arguments().size();
            }
            if (!matches) continue;

            Map<String, String> paramValues = new HashMap<>();
            for (int i = 0; i < ctor.parameters().size(); i++) {
                String pName = ((SingleVariableDeclaration) ctor.parameters().get(i)).getName().getIdentifier();
                Expression argExpr = (Expression) cic.arguments().get(i);
                List<String> consts = new ArrayList<>();
                if (constantExtractor != null) {
                    constantExtractor.extractConstantsFromArgument(argExpr, consts);
                }
                if (!consts.isEmpty()) {
                    paramValues.put(pName, consts.get(0));
                } else {
                    String resolved = null;
                    if (argExpr instanceof SimpleName sn && initialLocals != null && initialLocals.containsKey(sn.getIdentifier())) {
                        resolved = initialLocals.get(sn.getIdentifier());
                    }
                    if (resolved == null) {
                        resolved = constantResolver.resolve(argExpr, context);
                    }
                    if (resolved != null) paramValues.put(pName, resolved);
                }
            }
            // Evaluate constructor body regardless of whether parameters could be resolved as constants

            java.util.Map<String, String> localVars = new java.util.HashMap<>(paramValues);
            constantResolver.evaluateMethodBodyWithLocals(ctor, localVars, context, new java.util.HashSet<>());
            for (java.util.Map.Entry<String, String> entry : localVars.entrySet()) {
                if (entry.getKey().startsWith("this.")) {
                    fieldValues.put(entry.getKey(), entry.getValue());
                    fieldValues.put(entry.getKey().substring(5), entry.getValue());
                } else if (context.hasField(td, entry.getKey())) {
                    fieldValues.put(entry.getKey(), entry.getValue());
                    fieldValues.put("this." + entry.getKey(), entry.getValue());
                }
            }

            ctor.getBody().accept(new ASTVisitor() {
                @Override
                public boolean visit(SuperConstructorInvocation sci) {
                    IMethodBinding binding = sci.resolveConstructorBinding();
                    if (binding != null) {
                        ITypeBinding declaringClass = binding.getDeclaringClass();
                        if (declaringClass != null) {
                            TypeDeclaration superTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
                            if (superTd != null) {
                                processSuperConstructorArgs(superTd, binding, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
                                return super.visit(sci);
                            }
                        }
                    }
                    String superFqn = context.getSuperclassFqn(td);
                    if (superFqn != null) {
                        TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
                        if (superTd != null) {
                            processSuperConstructorArgs(superTd, null, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
                        }
                    }
                    return super.visit(sci);
                }

                @Override
                public boolean visit(ConstructorInvocation ci) {
                    IMethodBinding binding = ci.resolveConstructorBinding();
                    processSuperConstructorArgs(td, binding, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
                    return super.visit(ci);
                }
            });
            break; 
        }
    }

    private void processSuperConstructorArgs(
            TypeDeclaration superTd,
            IMethodBinding resolvedBinding,
            List<?> superArgs,
            Map<String, String> subClassParamValues,
            CodebaseContext context,
            Map<String, String> fieldValues,
            Set<String> visitedCtors,
            RhsResolver rhsResolver) {

        if (superTd == null) return;

        for (MethodDeclaration superCtor : superTd.getMethods()) {
            if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
            
            boolean matches = false;
            if (resolvedBinding != null && superCtor.resolveBinding() != null) {
                matches = resolvedBinding.isEqualTo(superCtor.resolveBinding()) || resolvedBinding.getKey().equals(superCtor.resolveBinding().getKey());
            } else {
                matches = superCtor.parameters().size() == superArgs.size();
            }
            if (!matches) continue;

            Map<String, String> superParamValues = new HashMap<>();
            for (int i = 0; i < superCtor.parameters().size(); i++) {
                String pName = ((SingleVariableDeclaration) superCtor.parameters().get(i)).getName().getIdentifier();
                Expression argExpr = (Expression) superArgs.get(i);
                
                String resolved = null;
                if (argExpr instanceof SimpleName sn) {
                    resolved = subClassParamValues.get(sn.getIdentifier());
                }
                if (resolved == null) {
                    List<String> consts = new ArrayList<>();
                    if (constantExtractor != null) {
                        constantExtractor.extractConstantsFromArgument(argExpr, consts);
                    }
                    if (!consts.isEmpty()) {
                        resolved = consts.get(0);
                    } else {
                        resolved = constantResolver.resolve(argExpr, context);
                    }
                }
                if (resolved == null && rhsResolver != null) {
                    TypeDeclaration enclosingTd = findEnclosingType(argExpr);
                    if (enclosingTd != null) {
                        resolved = rhsResolver.resolve(argExpr, subClassParamValues, enclosingTd);
                    }
                }
                if (resolved != null) {
                    superParamValues.put(pName, resolved);
                }
            }
            if (superParamValues.isEmpty()) continue;

            java.util.Map<String, String> localVars = new java.util.HashMap<>(superParamValues);
            constantResolver.evaluateMethodBodyWithLocals(superCtor, localVars, context, new java.util.HashSet<>());
            for (java.util.Map.Entry<String, String> entry : localVars.entrySet()) {
                if (entry.getKey().startsWith("this.")) {
                    fieldValues.put(entry.getKey(), entry.getValue());
                    fieldValues.put(entry.getKey().substring(5), entry.getValue());
                } else if (context.hasField(superTd, entry.getKey())) {
                    fieldValues.put(entry.getKey(), entry.getValue());
                    fieldValues.put("this." + entry.getKey(), entry.getValue());
                }
            }

            superCtor.getBody().accept(new ASTVisitor() {
                @Override
                public boolean visit(SuperConstructorInvocation sci) {
                    IMethodBinding binding = sci.resolveConstructorBinding();
                    if (binding != null) {
                        ITypeBinding declaringClass = binding.getDeclaringClass();
                        if (declaringClass != null) {
                            TypeDeclaration nextSuperTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
                            if (nextSuperTd != null) {
                                processSuperConstructorArgs(nextSuperTd, binding, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
                                return super.visit(sci);
                            }
                        }
                    }
                    String superFqn = context.getSuperclassFqn(superTd);
                    if (superFqn != null) {
                        TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
                        if (nextSuperTd != null) {
                            processSuperConstructorArgs(nextSuperTd, null, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
                        }
                    }
                    return super.visit(sci);
                }

                @Override
                public boolean visit(ConstructorInvocation ci) {
                    IMethodBinding binding = ci.resolveConstructorBinding();
                    processSuperConstructorArgs(superTd, binding, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
                    return super.visit(ci);
                }
            });
            break; 
        }
    }

    public String evaluateMethodCallInConstructor(
            Expression rhs,
            Map<String, String> paramValues,
            TypeDeclaration td) {

        if (!(rhs instanceof MethodInvocation mi)) {
            return null;
        }

        String methodName = mi.getName().getIdentifier();
        Expression receiver = mi.getExpression();
        if (receiver != null && !(receiver instanceof ThisExpression)) {
            return null;
        }

        MethodDeclaration method = context.findMethodDeclaration(td, methodName, true);
        if (method == null) {
            CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
            TypeDeclaration targetTd = context.resolveStaticImport(methodName, cu);
            if (targetTd != null) {
                method = context.findMethodDeclaration(targetTd, methodName, true);
            }
        }
        if (method == null || method.getBody() == null) {
            return null;
        }

        Map<String, String> localVars = new HashMap<>(paramValues);
        return constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new HashSet<>());
    }

    public ClassInstanceCreation findReturnedCIC(
            String className, String methodName, CodebaseContext context) {
        String cacheKey = className + "#" + methodName;
        if (returnedCicAbsent.contains(cacheKey)) {
            return null;
        }
        if (returnedCicCache.containsKey(cacheKey)) {
            return returnedCicCache.get(cacheKey);
        }
        ClassInstanceCreation resolved = findReturnedCIC(className, methodName, context, new HashSet<>(), 0);
        if (resolved != null) {
            returnedCicCache.put(cacheKey, resolved);
        } else {
            returnedCicAbsent.add(cacheKey);
        }
        return resolved;
    }

    private ClassInstanceCreation findReturnedCIC(
            String className, String methodName, CodebaseContext context,
            Set<String> visited, int depth) {
        if (ResolutionBudget.defaults().isReturnedCicExhausted(depth) || !visited.add(className + "#" + methodName)) {
            return null;
        }

        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td != null) {
            MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
            if (md != null && md.getBody() != null) {
                ClassInstanceCreation[] result = {null};
                md.getBody().accept(new ASTVisitor() {
                    @Override
                    public boolean visit(ReturnStatement rs) {
                        if (rs.getExpression() instanceof ClassInstanceCreation cic) {
                            result[0] = cic;
                        }
                        return false;
                    }
                });
                if (result[0] != null) return result[0];
            }
        }
        List<String> impls = context.getImplementations(className);
        for (String impl : impls) {
            ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context, visited, depth + 1);
            if (cic != null) return cic;
        }
        return null;
    }

    public List<String> resolveInlineInstantiationGetterResult(
            ClassInstanceCreation cic,
            String getterName,
            CodebaseContext context,
            Set<String> visited) {

        String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
        CompilationUnit contextCu = cic.getRoot() instanceof CompilationUnit ? (CompilationUnit) cic.getRoot() : null;
        AbstractTypeDeclaration atd = contextCu != null ? context.getAbstractTypeDeclaration(typeName, contextCu) : context.getAbstractTypeDeclaration(typeName);
        if (atd == null) atd = context.getAbstractTypeDeclaration(typeName);
        if (atd == null) return Collections.emptyList();

        if (atd instanceof RecordDeclaration rd) {
            int compIdx = -1;
            List<?> components = rd.recordComponents();
            for (int i = 0; i < components.size(); i++) {
                Object compObj = components.get(i);
                if (compObj != null) {
                    try {
                        java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
                        SimpleName nameObj = (SimpleName) getNameMethod.invoke(compObj);
                        if (nameObj != null && nameObj.getIdentifier().equals(getterName)) {
                            compIdx = i;
                            break;
                        }
                    } catch (Exception e) {
                        // ignore reflection errors
                    }
                }
            }
            if (compIdx >= 0 && compIdx < cic.arguments().size()) {
                Expression argExpr = (Expression) cic.arguments().get(compIdx);
                List<String> consts = new ArrayList<>();
                if (constantExtractor != null) {
                    constantExtractor.extractConstantsFromArgument(argExpr, consts);
                }
                if (!consts.isEmpty()) {
                    return consts;
                } else {
                    String resolved = constantResolver.resolve(argExpr, context);
                    if (resolved != null) {
                        return List.of(resolved);
                    }
                }
            }
            return Collections.emptyList();
        }

        if (!(atd instanceof TypeDeclaration td)) return Collections.emptyList();

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

        String getterKey = typeName + "#" + getterName;
        if (!visited.add(getterKey)) {
            return Collections.emptyList(); 
        }

        try {
            Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor);
            if (log.isDebugEnabled()) {
                log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
            }
            if (fieldValues.isEmpty()) return Collections.emptyList();

            String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
            if (log.isDebugEnabled()) {
                log.debug("RESOLVE_GETTER: result={}", result);
            }
            return result != null ? List.of(result) : Collections.emptyList();
        } finally {
            visited.remove(getterKey);
        }
    }

    public int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
        boolean isAllArgsConstructor = false;
        boolean isRequiredArgsConstructor = false;
        boolean isData = false;
        boolean isValue = false;
        
        for (Object modObj : td.modifiers()) {
            if (modObj instanceof MarkerAnnotation ma) {
                String name = ma.getTypeName().getFullyQualifiedName();
                if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
                if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
                if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
                if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
            } else if (modObj instanceof NormalAnnotation na) {
                String name = na.getTypeName().getFullyQualifiedName();
                if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
                if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
                if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
                if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
            }
        }
        
        if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) {
            return -1;
        }
        
        int index = 0;
        for (FieldDeclaration fd : td.getFields()) {
            boolean isStatic = false;
            boolean isFinal = false;
            boolean hasNonNull = false;
            for (Object modObj : fd.modifiers()) {
                if (modObj instanceof Modifier mod) {
                    if (mod.isStatic()) isStatic = true;
                    if (mod.isFinal()) isFinal = true;
                } else if (modObj instanceof MarkerAnnotation ma) {
                    if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true;
                }
            }
            if (isStatic) continue;
            
            if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
            if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
            
            for (Object fragObj : fd.fragments()) {
                VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                if (frag.getName().getIdentifier().equals(fieldName)) {
                    return index;
                }
                index++;
            }
        }
        return -1;
    }

    private void processConstructorInvocationArgs(List<?> arguments, TypeDeclaration targetTd, ASTNode callNode, List<String> results, String fieldName, CodebaseContext context, Set<String> visited) {
        if (targetTd == null) return;
        MethodDeclaration callerMd = findEnclosingMethod(callNode);
        
        IMethodBinding resolvedBinding = null;
        if (callNode instanceof ConstructorInvocation ci) {
            resolvedBinding = ci.resolveConstructorBinding();
        } else if (callNode instanceof SuperConstructorInvocation sci) {
            resolvedBinding = sci.resolveConstructorBinding();
        }
        
        for (MethodDeclaration otherMd : targetTd.getMethods()) {
            boolean matches = false;
            if (resolvedBinding != null && otherMd.resolveBinding() != null) {
                matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
            } else {
                matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd;
            }
            
            if (matches) {
                int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new HashSet<>());
                if (targetIdx >= 0 && targetIdx < arguments.size()) {
                    Expression arg = (Expression) arguments.get(targetIdx);
                    String val = constantResolver.resolve(arg, context);
                    if (val == null && arg instanceof MethodInvocation mi) {
                        TypeDeclaration enclosingTd = findEnclosingType(arg);
                        if (enclosingTd != null) {
                            val = evaluateMethodCallInConstructor(arg, new HashMap<>(), enclosingTd);
                        }
                    }
                    if (val != null) {
                        if (val.startsWith("ENUM_SET:")) {
                            for (String eVal : val.substring(9).split(",")) {
                                if (constantExtractor != null) {
                                    results.add(constantExtractor.parseEnumSetElement(eVal));
                                }
                            }
                        } else {
                            results.add(val);
                        }
                    }
                }
            }
        }
    }

    /**
     * Maps an inherited field supplied through {@code super(...)} to a subclass constructor parameter type.
     * Returns null when the mapping cannot be proven.
     */
    public String resolveSubclassConstructorParameterFqnForInheritedField(
            TypeDeclaration subclassTd,
            String inheritedFieldName,
            click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer injectionAnalyzer) {
        if (subclassTd == null || inheritedFieldName == null || inheritedFieldName.isBlank()) {
            return null;
        }
        for (MethodDeclaration ctor : subclassTd.getMethods()) {
            if (!ctor.isConstructor() || ctor.getBody() == null) {
                continue;
            }
            int paramIdx = findAssignedParameterIndex(ctor, inheritedFieldName, context, new HashSet<>());
            if (paramIdx < 0 || paramIdx >= ctor.parameters().size()) {
                continue;
            }
            SingleVariableDeclaration param = (SingleVariableDeclaration) ctor.parameters().get(paramIdx);
            IVariableBinding paramBinding = param.resolveBinding();
            if (injectionAnalyzer != null && paramBinding != null) {
                String injectedBean = injectionAnalyzer.resolveInjectedBeanFqn(paramBinding);
                if (injectedBean != null && !injectedBean.isBlank()) {
                    return injectedBean;
                }
            }
            if (paramBinding != null && paramBinding.getType() != null) {
                String fqn = paramBinding.getType().getErasure().getQualifiedName();
                if (fqn != null && !fqn.isBlank() && !"java.lang.Object".equals(fqn)) {
                    return fqn;
                }
            }
            CompilationUnit cu = subclassTd.getRoot() instanceof CompilationUnit root ? root : null;
            String astFqn = resolveTypeToFqn(param.getType(), cu);
            if (astFqn != null && !astFqn.isBlank()) {
                return astFqn;
            }
        }
        return null;
    }

    /** Visible for unit tests verifying constructor super() parameter mapping. */
    int findAssignedParameterIndexForTest(MethodDeclaration constructorMd, String fieldName) {
        return findAssignedParameterIndex(constructorMd, fieldName, context, new HashSet<>());
    }

    private String resolveTypeToFqn(Type type, CompilationUnit cu) {
        if (type == null) {
            return null;
        }
        String raw = type.toString();
        if (raw.contains("<")) {
            raw = raw.substring(0, raw.indexOf('<'));
        }
        TypeDeclaration td = cu != null ? context.getTypeDeclaration(raw, cu) : context.getTypeDeclaration(raw);
        return td != null ? context.getFqn(td) : raw;
    }

    private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, Set<MethodDeclaration> visited) {
        if (constructorMd == null || constructorMd.getBody() == null) return -1;
        if (!visited.add(constructorMd)) return -1;
        
        final int[] foundIdx = {-1};
        constructorMd.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(Assignment asn) {
                Expression left = asn.getLeftHandSide();
                if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) ||
                    (left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) ||
                    (left instanceof SuperFieldAccess sfa && sfa.getName().getIdentifier().equals(fieldName))) {
                    Expression right = asn.getRightHandSide();
                    String rightName = extractVariableName(right);
                    if (rightName != null) {
                        for (int i = 0; i < constructorMd.parameters().size(); i++) {
                            SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
                            if (svd.getName().getIdentifier().equals(rightName)) {
                                foundIdx[0] = i;
                            }
                        }
                    }
                }
                return super.visit(asn);
            }
            @Override
            public boolean visit(ConstructorInvocation node) {
                TypeDeclaration enclosingTd = findEnclosingType(node);
                if (enclosingTd != null) {
                    MethodDeclaration targetCtor = selectDelegatedConstructor(
                            enclosingTd.getMethods(),
                            node.resolveConstructorBinding(),
                            node.arguments().size(),
                            constructorMd);
                    mapDelegatedConstructorArgument(targetCtor, node.arguments(), fieldName, constructorMd, foundIdx, visited);
                }
                return super.visit(node);
            }
            @Override
            public boolean visit(SuperConstructorInvocation node) {
                TypeDeclaration enclosingTd = findEnclosingType(node);
                if (enclosingTd != null) {
                    String superFqn = context.getSuperclassFqn(enclosingTd);
                    if (superFqn != null) {
                        TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
                        if (superTd != null) {
                            MethodDeclaration targetCtor = selectDelegatedConstructor(
                                    superTd.getMethods(),
                                    node.resolveConstructorBinding(),
                                    node.arguments().size(),
                                    null);
                            mapDelegatedConstructorArgument(targetCtor, node.arguments(), fieldName, constructorMd, foundIdx, visited);
                        }
                    }
                }
                return super.visit(node);
            }
        });
        return foundIdx[0];
    }

    /**
     * Prefer JDT binding identity when available. If the binding is missing or does not map to a
     * source constructor, fall back to a unique same-arity constructor. Ambiguous same-arity
     * overloads without a binding match return null (do not guess).
     */
    private static MethodDeclaration selectDelegatedConstructor(
            MethodDeclaration[] candidates,
            IMethodBinding resolvedBinding,
            int argumentCount,
            MethodDeclaration exclude) {
        if (candidates == null) {
            return null;
        }
        if (resolvedBinding != null) {
            for (MethodDeclaration otherMd : candidates) {
                if (!otherMd.isConstructor() || otherMd == exclude) {
                    continue;
                }
                IMethodBinding otherBinding = otherMd.resolveBinding();
                if (otherBinding != null && constructorBindingsMatch(resolvedBinding, otherBinding)) {
                    return otherMd;
                }
            }
        }
        MethodDeclaration arityMatch = null;
        for (MethodDeclaration otherMd : candidates) {
            if (!otherMd.isConstructor() || otherMd == exclude) {
                continue;
            }
            if (otherMd.parameters().size() != argumentCount) {
                continue;
            }
            if (arityMatch != null) {
                // Ambiguous same-arity overloads without a binding hit — do not guess.
                return null;
            }
            arityMatch = otherMd;
        }
        return arityMatch;
    }

    private static boolean constructorBindingsMatch(IMethodBinding left, IMethodBinding right) {
        if (left.isEqualTo(right) || Objects.equals(left.getKey(), right.getKey())) {
            return true;
        }
        if (!Objects.equals(left.getName(), right.getName())) {
            return false;
        }
        ITypeBinding[] leftParams = left.getParameterTypes();
        ITypeBinding[] rightParams = right.getParameterTypes();
        if (leftParams.length != rightParams.length) {
            return false;
        }
        for (int i = 0; i < leftParams.length; i++) {
            if (!leftParams[i].getErasure().isEqualTo(rightParams[i].getErasure())) {
                return false;
            }
        }
        ITypeBinding leftDecl = left.getDeclaringClass();
        ITypeBinding rightDecl = right.getDeclaringClass();
        return leftDecl != null && rightDecl != null && leftDecl.getErasure().isEqualTo(rightDecl.getErasure());
    }

    private void mapDelegatedConstructorArgument(
            MethodDeclaration targetCtor,
            List<?> arguments,
            String fieldName,
            MethodDeclaration constructorMd,
            int[] foundIdx,
            Set<MethodDeclaration> visited) {
        if (targetCtor == null || arguments == null) {
            return;
        }
        int targetIdx = findAssignedParameterIndex(targetCtor, fieldName, context, visited);
        if (targetIdx < 0 || targetIdx >= arguments.size()) {
            return;
        }
        Expression arg = (Expression) arguments.get(targetIdx);
        String argName = extractVariableName(arg);
        if (argName == null) {
            return;
        }
        for (int i = 0; i < constructorMd.parameters().size(); i++) {
            SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
            if (svd.getName().getIdentifier().equals(argName)) {
                foundIdx[0] = i;
                return;
            }
        }
    }

    private String extractVariableName(Expression expr) {
        if (expr instanceof SimpleName sn) return sn.getIdentifier();
        if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier();
        if (expr instanceof SuperFieldAccess sfa) return sfa.getName().getIdentifier();
        if (expr instanceof CastExpression ce) return extractVariableName(ce.getExpression());
        if (expr instanceof ParenthesizedExpression pe) return extractVariableName(pe.getExpression());
        if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
            if (!mi.arguments().isEmpty()) return extractVariableName((Expression)mi.arguments().get(0));
        }
        return null;
    }

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