package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.pipeline.EventArgumentSelector;
import click.kamil.springstatemachineexporter.analysis.pipeline.MessageCarrierSupport;
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

@Slf4j
public class GenericEventDetector {
    private final CodebaseContext context;
    private final ConstantResolver constantResolver;
    private final List<LibraryHint> hints;
    private final TypeResolver typeResolver;

    public GenericEventDetector(CodebaseContext context, ConstantResolver constantResolver, List<LibraryHint> hints) {
        this.context = context;
        this.constantResolver = constantResolver;
        this.hints = hints;
        this.typeResolver = new TypeResolver(context);
    }

    private static final Set<String> TRIGGER_METHOD_NAMES = Set.of(
            "sendEvent", "sendEvents", "sendEventCollect", "sendEventMono", "fire", "trigger");

    /** Built-in source patterns for Spring State Machine and messaging APIs (no hints.json required). */
    private static final List<LibraryHint> BUILTIN_SOURCE_PATTERNS = List.of(
            LibraryHint.builder().methodFqn("org.springframework.messaging.support.MessageBuilder.build").eventArgumentIndex(0).build(),
            LibraryHint.builder().methodFqn("org.springframework.messaging.support.GenericMessage.getPayload").eventArgumentMethod("getPayload").build());

    public List<TriggerPoint> detect(CompilationUnit cu) {
        List<TriggerPoint> triggers = new ArrayList<>();
        String fileName = "unknown";
        try {
            if (cu.getJavaElement() != null) fileName = cu.getJavaElement().getElementName();
        } catch (Exception e) {}
        
        log.debug("Scanning for triggers in {}", fileName);
        
        cu.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                String methodName = node.getName().getIdentifier();
                
                if (TRIGGER_METHOD_NAMES.contains(methodName)) {
                    processSendEvent(node, cu, triggers);
                } else {
                    for (Object argObj : node.arguments()) {
                        if (argObj instanceof ExpressionMethodReference emr) {
                            String refMethod = emr.getName().getIdentifier();
                            if (TRIGGER_METHOD_NAMES.contains(refMethod)) {
                                processSendEventMethodReference(node, emr, cu, triggers);
                            }
                        }
                    }
                }
                
                processBuiltInPatterns(node, cu, triggers);
                processHints(node, cu, triggers);

                return super.visit(node);
            }
        });
        return triggers;
    }

    private void processSendEventMethodReference(MethodInvocation node, ExpressionMethodReference emr, CompilationUnit cu, List<TriggerPoint> triggers) {
        Expression receiver = node.getExpression();
        String eventValue = extractEventFromReceiver(receiver);
        if (eventValue == null) {
            eventValue = receiver != null ? receiver.toString() : null;
        }

        MethodDeclaration method = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
        AbstractTypeDeclaration type = findEnclosingAbstractType(node);
        if (type == null) return;

        String sourceState = extractSourceState(node);
        if (sourceState == null) {
            sourceState = extractSourceStateFromArguments(node);
        }
        String stateMachineId = extractStateMachineId(node);
        String[] smTypes = resolveStateMachineTypeArgumentsForExpression(emr.getExpression(), node);

        boolean external = false;
        if (receiver != null) {
            AssignmentDag dag = new AssignmentDag(method);
            external = dag.isExternal(receiver, new java.util.HashSet<>());
        }

        if (eventValue != null) {
            String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
            triggers.add(TriggerPoint.builder()
                    .event(eventValue)
                    .sourceState(sourceState)
                    .stateMachineId(stateMachineId)
                    .className(context.getFqn(type))
                    .methodName(method != null ? method.getName().getIdentifier() : "initializer")
                    .sourceFile(context.getRelativePath(context.getFqn(type)))
                    .lineNumber(cu.getLineNumber(node.getStartPosition()))
                    .stateTypeFqn(smTypes[0])
                    .eventTypeFqn(smTypes[1])
                    .external(external)
                    .constraint(constraint)
                    .build());
        }
    }

    private String extractEventFromReceiver(Expression receiver) {
        String reactivePayload = ReactiveExpressionSupport.extractPayload(receiver, constantResolver, context);
        if (reactivePayload != null) {
            return reactivePayload;
        }
        if (receiver == null) return null;
        Expression factoryPayload = ReactiveExpressionSupport.peelFactoryPayloadExpression(receiver);
        if (factoryPayload != null) {
            String resolved = constantResolver.resolve(factoryPayload, context);
            return resolved != null ? resolved : factoryPayload.toString();
        }
        if (receiver instanceof SimpleName sn) {
            return sn.getIdentifier();
        }
        return null;
    }

    private String[] resolveStateMachineTypeArgumentsForExpression(Expression receiver, MethodInvocation node) {
        if (receiver == null) return new String[]{null, null};

        ITypeBinding binding = receiver.resolveTypeBinding();
        if (binding != null) {
            ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>());
            if (smBinding != null) {
                ITypeBinding[] typeArgs = smBinding.getTypeArguments();
                if (typeArgs.length >= 2) {
                    return new String[]{
                            concreteStateMachineTypeArgFqn(typeArgs[0]),
                            concreteStateMachineTypeArgFqn(typeArgs[1])};
                }
            }
        }

        Type declType = findReceiverTypeManually(receiver, node);
        if (declType instanceof ParameterizedType pt) {
            List<?> typeArgs = pt.typeArguments();
            if (typeArgs.size() >= 2) {
                CompilationUnit compilationUnit = (CompilationUnit) node.getRoot();
                String stateType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(0), compilationUnit);
                String eventType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(1), compilationUnit);
                if (isUnresolvedTypeVariableName(stateType)) {
                    stateType = null;
                }
                if (isUnresolvedTypeVariableName(eventType)) {
                    eventType = null;
                }
                return new String[]{stateType, eventType};
            }
        }

        return new String[]{null, null};
    }

    private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
        List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
        if (builtTriggers != null) {
            for (TriggerPoint trigger : builtTriggers) {
                if (log.isDebugEnabled()) {
                    log.debug("Successfully built trigger point: {}", trigger.getEvent());
                }
                triggers.add(trigger);
            }
        }
    }

    private void processBuiltInPatterns(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
        String methodName = node.getName().getIdentifier();
        if (TRIGGER_METHOD_NAMES.contains(methodName)) {
            return;
        }
        String calledMethod = resolveCalledMethodName(node);
        if (calledMethod == null) {
            return;
        }
        for (LibraryHint pattern : BUILTIN_SOURCE_PATTERNS) {
            if (!matchesHint(calledMethod, pattern.getMethodFqn())) {
                continue;
            }
            String eventToUse = resolveHintEvent(node, pattern);
            List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, eventToUse);
            if (builtTriggers != null) {
                for (TriggerPoint trigger : builtTriggers) {
                    if (log.isDebugEnabled()) {
                        log.debug("Built trigger from built-in source pattern {}: {}", pattern.getMethodFqn(), trigger.getEvent());
                    }
                    triggers.add(trigger);
                }
            }
        }
    }

    private void processHints(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
        if (hints == null || hints.isEmpty()) {
            return;
        }

        String calledMethod = resolveCalledMethodName(node);
        if (calledMethod == null) {
            return;
        }

        for (LibraryHint hint : hints) {
            if (!matchesHint(calledMethod, hint.getMethodFqn())) {
                continue;
            }
            if (log.isDebugEnabled()) {
                log.debug("Applying optional hints.json override for {}", hint.getMethodFqn());
            }
            String eventToUse = resolveHintEvent(node, hint);
            List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, eventToUse);
            if (builtTriggers != null) {
                for (TriggerPoint trigger : builtTriggers) {
                    if (log.isDebugEnabled()) {
                        log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
                    }
                    triggers.add(trigger);
                }
            }
        }
    }

    private static String resolveHintEvent(MethodInvocation node, LibraryHint hint) {
        String eventToUse = hint.getEvent();
        if (eventToUse == null && hint.getEventArgumentIndex() != null) {
            if (node.arguments().size() > hint.getEventArgumentIndex()) {
                Expression argExpr = (Expression) node.arguments().get(hint.getEventArgumentIndex());
                eventToUse = argExpr.toString();
                if (hint.getEventArgumentMethod() != null && !hint.getEventArgumentMethod().isEmpty()) {
                    eventToUse = eventToUse + "." + hint.getEventArgumentMethod() + "()";
                }
            }
        }
        return eventToUse;
    }

    private static boolean matchesHint(String calledMethod, String hintFqn) {
        return calledMethod.equals(hintFqn) || hintFqn.endsWith("." + calledMethod);
    }

    private String resolveCalledMethodName(MethodInvocation node) {
        Expression receiver = node.getExpression();
        String methodName = node.getName().getIdentifier();
        if (receiver == null) return methodName;

        if (receiver instanceof SimpleName sn) {
            String name = sn.getIdentifier();
            AbstractTypeDeclaration enclosing = findEnclosingAbstractType(node);
            if (enclosing != null) {
                Type memberType = lookupMemberType(enclosing, name);
                if (memberType != null) {
                    return memberType + "." + methodName;
                }
            }
            return name + "." + methodName;
        }
        
        return receiver.toString() + "." + methodName;
    }

    private List<TriggerPoint> buildTriggerPoints(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
        String eventValue = null;
        if (forcedEvent != null) {
            eventValue = context.resolveString(forcedEvent);
        } else {
            if (node.arguments().isEmpty()) return Collections.emptyList();
            Expression eventExpr = EventArgumentSelector.selectEventArgument(node).orElse(null);
            if (eventExpr == null) {
                return Collections.emptyList();
            }

            // Try MessageBuilder extraction first
            eventValue = extractEventFromMessageBuilder(eventExpr);
            
            if (eventValue != null) {
                 eventValue = context.resolveString(eventValue);
            } else {
                 eventValue = context.resolveExpression(eventExpr);
            }
            String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
                    eventExpr, constantResolver, context);
            if (remapped != null) {
                eventValue = remapped;
            }
        }

        MethodDeclaration method = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
        AbstractTypeDeclaration type = findEnclosingAbstractType(node);

        if (type == null) return Collections.emptyList();

        String sourceState = extractSourceState(node);
        if (sourceState == null && node instanceof MethodInvocation mi) {
            Expression selected = EventArgumentSelector.selectEventArgument(mi).orElse(null);
            if (EventArgumentSelector.selectedIsFirstArgument(mi, selected)) {
                sourceState = extractSourceStateFromArguments(mi);
            }
        }
        String stateMachineId = extractStateMachineId(node);
        String[] smTypes = resolveStateMachineTypeArguments(node);

        boolean external = false;
        if (!node.arguments().isEmpty()) {
            Expression eventExpr = EventArgumentSelector.selectEventArgument(node).orElse(null);
            if (eventExpr != null) {
                AssignmentDag dag = new AssignmentDag(method);
                external = dag.isExternal(eventExpr, new java.util.HashSet<>());
            }
        }
        
        String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
        
        List<TriggerPoint> results = new ArrayList<>();
        if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
            String[] parts = eventValue.substring(9).split(",");
            for (String part : parts) {
                if (!part.trim().isEmpty()) {
                    results.add(TriggerPoint.builder()
                            .event(part.trim())
                            .sourceState(sourceState)
                            .stateMachineId(stateMachineId)
                            .className(context.getFqn(type))
                            .methodName(method != null ? method.getName().getIdentifier() : "initializer")
                            .sourceFile(context.getRelativePath(context.getFqn(type)))
                            .lineNumber(cu.getLineNumber(node.getStartPosition()))
                            .stateTypeFqn(smTypes[0])
                            .eventTypeFqn(smTypes[1])
                            .external(external)
                            .constraint(constraint)
                            .build());
                }
            }
        } else {
            results.add(TriggerPoint.builder()
                    .event(eventValue)
                    .sourceState(sourceState)
                    .stateMachineId(stateMachineId)
                    .className(context.getFqn(type))
                    .methodName(method != null ? method.getName().getIdentifier() : "initializer")
                    .sourceFile(context.getRelativePath(context.getFqn(type)))
                    .lineNumber(cu.getLineNumber(node.getStartPosition()))
                    .stateTypeFqn(smTypes[0])
                    .eventTypeFqn(smTypes[1])
                    .external(external)
                    .constraint(constraint)
                    .build());
        }

        return results;
    }

    private String extractSourceState(ASTNode node) {
        ASTNode current = node;
        while (current != null && !(current instanceof MethodDeclaration)) {
            ASTNode parent = current.getParent();
            
            // 1. Direct parent is IfStatement wrapper (e.g., if (state == X) { sm.sendEvent() })
            if (parent instanceof IfStatement ifStmt) {
                // Only consider if our current node is in the 'then' or 'else' part, not the condition
                if (ifStmt.getExpression() != current) {
                    Expression expr = ifStmt.getExpression();
                    String state = extractStateFromExpression(expr);
                    if (state != null) return state;
                }
            }

            // 2. Parent is a Block or SwitchStatement: perform sibling traversal
            if (parent instanceof Block block) {
                String state = extractStateFromSiblings(current, block.statements());
                if (state != null) return state;
            } else if (parent instanceof SwitchStatement switchStmt) {
                if (isSourceStateSelector(switchStmt.getExpression())) {
                    String state = extractStateFromSiblings(current, switchStmt.statements());
                    if (state != null) return state;
                }
            } else if (parent instanceof SwitchExpression switchExpr) {
                if (isSourceStateSelector(switchExpr.getExpression())) {
                    String state = extractStateFromSiblings(current, switchExpr.statements());
                    if (state != null) return state;
                }
            } else if (parent instanceof SwitchCase switchCase && !switchCase.isDefault()) {
                Expression selector = resolveSwitchSelector(switchCase);
                if (selector != null
                        && isSourceStateSelector(selector)
                        && !switchCase.expressions().isEmpty()) {
                    Expression caseExpr = (Expression) switchCase.expressions().get(0);
                    // String case labels are routing keys (JMS payload / command), not state enums.
                    if (caseExpr instanceof StringLiteral) {
                        current = parent;
                        continue;
                    }
                    String resolved = resolveProvableStateLiteral(caseExpr);
                    if (resolved != null) {
                        return resolved;
                    }
                    return getSimpleNameString(caseExpr);
                }
            }

            current = parent;
        }
        return null;
    }

    private Expression resolveSwitchSelector(SwitchCase switchCase) {
        ASTNode switchParent = switchCase.getParent();
        if (switchParent instanceof SwitchStatement switchStatement) {
            return switchStatement.getExpression();
        }
        if (switchParent instanceof SwitchExpression switchExpression) {
            return switchExpression.getExpression();
        }
        return null;
    }

    private String extractStateMachineId(MethodInvocation node) {
        if (!TRIGGER_METHOD_NAMES.contains(node.getName().getIdentifier())) {
            return null;
        }
        Expression receiver = node.getExpression();
        while (receiver instanceof MethodInvocation methodInvocation) {
            receiver = methodInvocation.getExpression();
        }
        if (receiver == null) {
            return null;
        }
        AbstractTypeDeclaration enclosing = findEnclosingAbstractType(node);
        if (enclosing == null) {
            return null;
        }
        return resolveQualifierForReceiver(receiver, enclosing);
    }

    private String resolveQualifierForReceiver(Expression receiver, AbstractTypeDeclaration enclosing) {
        String fieldName = null;
        if (receiver instanceof SimpleName simpleName) {
            fieldName = simpleName.getIdentifier();
            IBinding binding = simpleName.resolveBinding();
            if (binding instanceof IVariableBinding variableBinding) {
                String fromBinding = extractQualifierFromBinding(variableBinding);
                if (fromBinding != null) {
                    return fromBinding;
                }
            }
        } else if (receiver instanceof FieldAccess fieldAccess) {
            fieldName = fieldAccess.getName().getIdentifier();
        }
        if (fieldName == null) {
            return null;
        }
        return findQualifierOnField(enclosing, fieldName);
    }

    private String findQualifierOnField(AbstractTypeDeclaration enclosing, String fieldName) {
        for (FieldDeclaration fieldDeclaration : getFieldDeclarations(enclosing)) {
            for (Object fragmentObj : fieldDeclaration.fragments()) {
                if (fragmentObj instanceof VariableDeclarationFragment fragment
                        && fragment.getName().getIdentifier().equals(fieldName)) {
                    String qualifier = extractQualifierAnnotation(fieldDeclaration.modifiers());
                    if (qualifier != null) {
                        return qualifier;
                    }
                }
            }
        }
        if (enclosing instanceof TypeDeclaration typeDeclaration) {
            for (MethodDeclaration method : typeDeclaration.getMethods()) {
                if (!hasAutowiredAnnotation(method) || method.parameters().size() != 1) {
                    continue;
                }
                SingleVariableDeclaration param = (SingleVariableDeclaration) method.parameters().get(0);
                if (!matchesInjectionTarget(method, fieldName, param)) {
                    continue;
                }
                String qualifier = extractQualifierAnnotation(param.modifiers());
                if (qualifier != null) {
                    return qualifier;
                }
            }
        }
        return null;
    }

    private static boolean hasAutowiredAnnotation(MethodDeclaration method) {
        for (Object modifierObj : method.modifiers()) {
            if (modifierObj instanceof Annotation annotation
                    && annotation.getTypeName().toString().endsWith("Autowired")) {
                return true;
            }
        }
        return false;
    }

    private static boolean matchesInjectionTarget(
            MethodDeclaration method, String fieldName, SingleVariableDeclaration param) {
        if (fieldName.equals(param.getName().getIdentifier())) {
            return true;
        }
        String methodName = method.getName().getIdentifier();
        if (methodName.startsWith("set") && methodName.length() > 3) {
            String propertyName = methodName.substring(3);
            return propertyName.equalsIgnoreCase(fieldName)
                    || (Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1)).equals(fieldName);
        }
        return false;
    }

    private String extractQualifierFromBinding(IVariableBinding binding) {
        if (binding == null) {
            return null;
        }
        String qualifier = readQualifierFromAnnotations(binding.getAnnotations());
        if (qualifier != null) {
            return qualifier;
        }
        if (binding.isField() && binding.getDeclaringClass() != null) {
            for (IMethodBinding method : binding.getDeclaringClass().getDeclaredMethods()) {
                boolean isAutowiredMethod = false;
                for (IAnnotationBinding ann : method.getAnnotations()) {
                    if (ann.getAnnotationType() != null
                            && "org.springframework.beans.factory.annotation.Autowired"
                                    .equals(ann.getAnnotationType().getQualifiedName())) {
                        isAutowiredMethod = true;
                        break;
                    }
                }
                if (!method.isConstructor() && !isAutowiredMethod) {
                    continue;
                }
                for (int i = 0; i < method.getParameterTypes().length; i++) {
                    if (!method.getParameterTypes()[i].isEqualTo(binding.getType().getErasure())) {
                        continue;
                    }
                    if (method.isConstructor() && !binding.getName().equals(method.getParameterNames()[i])) {
                        continue;
                    }
                    if (!method.isConstructor() && !matchesSetterParameterName(method.getName(), binding.getName())) {
                        continue;
                    }
                    String paramQualifier = readQualifierFromAnnotations(method.getParameterAnnotations(i));
                    if (paramQualifier != null) {
                        return paramQualifier;
                    }
                }
            }
        }
        return null;
    }

    private static boolean matchesSetterParameterName(String methodName, String fieldName) {
        if (methodName == null || fieldName == null) {
            return false;
        }
        if (methodName.startsWith("set") && methodName.length() > 3) {
            String propertyName = methodName.substring(3);
            return propertyName.equalsIgnoreCase(fieldName)
                    || (Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1)).equals(fieldName);
        }
        return fieldName.equals(methodName);
    }

    private String readQualifierFromAnnotations(IAnnotationBinding[] annotations) {
        if (annotations == null) {
            return null;
        }
        for (IAnnotationBinding annotation : annotations) {
            if (annotation.getAnnotationType() == null
                    || !"org.springframework.beans.factory.annotation.Qualifier"
                            .equals(annotation.getAnnotationType().getQualifiedName())) {
                continue;
            }
            for (IMemberValuePairBinding pair : annotation.getDeclaredMemberValuePairs()) {
                Object value = pair.getValue();
                if (value instanceof String stringValue) {
                    return stringValue;
                }
            }
        }
        return null;
    }

    private String extractQualifierAnnotation(List<?> modifiers) {
        if (modifiers == null) {
            return null;
        }
        for (Object modifierObj : modifiers) {
            if (!(modifierObj instanceof Annotation annotation)) {
                continue;
            }
            String typeName = annotation.getTypeName().toString();
            if (!typeName.endsWith("Qualifier")) {
                continue;
            }
            String value = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(
                    annotation, "value");
            if (value == null || value.isBlank()) {
                value = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(
                        annotation, "name");
            }
            return stripAnnotationQuotes(value);
        }
        return null;
    }

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

    /**
     * Infers source state from a literal second argument to sendEvent(event, sourceState) when provable from AST.
     */
    private String extractSourceStateFromArguments(MethodInvocation node) {
        if (node.arguments().size() < 2) {
            return null;
        }
        Expression stateExpr = (Expression) node.arguments().get(1);
        return resolveProvableStateLiteral(stateExpr);
    }

    private String resolveProvableStateLiteral(Expression expr) {
        if (expr == null) {
            return null;
        }
        Expression peeled = expr;
        while (peeled instanceof ParenthesizedExpression pe) {
            peeled = pe.getExpression();
        }
        while (peeled instanceof CastExpression ce) {
            peeled = ce.getExpression();
        }
        if (peeled instanceof QualifiedName || peeled instanceof FieldAccess || peeled instanceof StringLiteral) {
            String resolved = constantResolver.resolve(peeled, context);
            if (resolved != null && !resolved.isBlank()) {
                return getSimpleNameString(peeled);
            }
            if (peeled instanceof StringLiteral sl) {
                return sl.getLiteralValue();
            }
        }
        return null;
    }

    private String extractStateFromSiblings(ASTNode currentNode, List<?> statements) {
        int index = statements.indexOf(currentNode);
        if (index <= 0) return null; // No previous siblings

        // Walk backwards through siblings
        for (int i = index - 1; i >= 0; i--) {
            Object stmtObj = statements.get(i);
            
            if (stmtObj instanceof SwitchCase switchCase) {
                if (!switchCase.expressions().isEmpty()) {
                    Expression caseExpr = (Expression) switchCase.expressions().get(0);
                    // String case labels are routing keys, not SM source states.
                    if (caseExpr instanceof StringLiteral) {
                        continue;
                    }
                    return getSimpleNameString(caseExpr);
                }
            } else if (stmtObj instanceof IfStatement ifStmt) {
                // Guard clause detection: check if the 'then' block has a return/throw
                if (isGuardClause(ifStmt.getThenStatement())) {
                    Expression expr = ifStmt.getExpression();
                    // If it's a guard clause, it usually checks `state != EXPECTED`, so the true state *is* EXPECTED
                    String state = extractStateFromExpression(expr);
                    if (state != null) return state;
                }
            }
        }
        return null;
    }

    private boolean isGuardClause(Statement thenStatement) {
        if (thenStatement instanceof ReturnStatement || thenStatement instanceof ThrowStatement) {
            return true;
        }
        if (thenStatement instanceof Block block) {
            for (Object obj : block.statements()) {
                if (obj instanceof ReturnStatement || obj instanceof ThrowStatement) {
                    return true;
                }
            }
        }
        return false;
    }

    private String extractStateFromExpression(Expression expr) {
        if (expr instanceof InfixExpression infix) {
            if (infix.getOperator() == InfixExpression.Operator.EQUALS || infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) {
                Expression left = infix.getLeftOperand();
                Expression right = infix.getRightOperand();
                
                // Ignore null checks entirely
                if (left instanceof NullLiteral || right instanceof NullLiteral) {
                    return null;
                }

                if (isRoutingDiscriminant(left) || isRoutingDiscriminant(right)) {
                    return null;
                }

                String leftState = resolveProvableStateLiteral(left);
                if (leftState != null) {
                    return leftState;
                }
                String rightState = resolveProvableStateLiteral(right);
                if (rightState != null) {
                    return rightState;
                }
            }
        } else if (expr instanceof MethodInvocation mi) {
            String methodName = mi.getName().getIdentifier();
            if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
                Expression receiver = mi.getExpression();
                Expression arg = (Expression) mi.arguments().get(0);

                if (isRoutingDiscriminant(receiver) || isRoutingDiscriminant(arg)) {
                    return null;
                }

                String receiverState = resolveProvableStateLiteral(receiver);
                if (receiverState != null) {
                    return receiverState;
                }
                return resolveProvableStateLiteral(arg);
            }
        }
        return null;
    }

    /**
     * Positive evidence that a switch/if selector is an SM source state (not a payload/route key).
     */
    private boolean isSourceStateSelector(Expression expr) {
        if (expr == null || isRoutingDiscriminant(expr)) {
            return false;
        }
        if (expr instanceof SimpleName sn) {
            if (EventCarrierPolicy.isStateAccessorName(sn.getIdentifier())) {
                return true;
            }
            if (isCarrierStringParameter(sn)) {
                return false;
            }
            return isLikelyStateEnumType(sn);
        }
        if (expr instanceof MethodInvocation mi) {
            String name = mi.getName().getIdentifier();
            if (EventCarrierPolicy.isStateAccessorMethodName(name)) {
                return true;
            }
        }
        return isLikelyStateEnumType(expr);
    }

    private boolean isRoutingDiscriminant(Expression expr) {
        if (expr instanceof SimpleName sn) {
            if (EventCarrierPolicy.isRoutingDiscriminantName(sn.getIdentifier())) {
                return true;
            }
            return isCarrierStringParameter(sn);
        }
        return false;
    }

    /**
     * {@code String} method parameter that carries a payload/event key (by name, {@code @Payload},
     * or messaging body convention) — never a source-state selector.
     */
    private boolean isCarrierStringParameter(SimpleName name) {
        MethodDeclaration method = click.kamil.springstatemachineexporter.ast.common.AstUtils
                .findEnclosingMethod(name);
        if (method == null) {
            return false;
        }
        for (Object paramObj : method.parameters()) {
            if (!(paramObj instanceof SingleVariableDeclaration param)) {
                continue;
            }
            if (!param.getName().getIdentifier().equals(name.getIdentifier())) {
                continue;
            }
            if (!isStringType(param.getType())) {
                return false;
            }
            if (hasPayloadAnnotation(param)) {
                return true;
            }
            if (EventCarrierPolicy.isCarrierParamName(param.getName().getIdentifier())) {
                return true;
            }
            // Sole unannotated String param on the method — same rule as messaging body carrier.
            return countUnannotatedStringParams(method) == 1;
        }
        return false;
    }

    private static int countUnannotatedStringParams(MethodDeclaration method) {
        int count = 0;
        for (Object paramObj : method.parameters()) {
            if (!(paramObj instanceof SingleVariableDeclaration param)) {
                continue;
            }
            if (hasPayloadAnnotation(param)) {
                continue;
            }
            boolean unannotated = true;
            for (Object modifier : param.modifiers()) {
                if (modifier instanceof Annotation) {
                    unannotated = false;
                    break;
                }
            }
            if (unannotated && isStringType(param.getType())) {
                count++;
            }
        }
        return count;
    }

    private static boolean isStringType(org.eclipse.jdt.core.dom.Type type) {
        if (type == null) {
            return false;
        }
        return MachineEnumCanonicalizer.isJavaStringType(type.toString());
    }

    private static boolean hasPayloadAnnotation(SingleVariableDeclaration param) {
        for (Object modifier : param.modifiers()) {
            if (modifier instanceof Annotation annotation) {
                String typeName = annotation.getTypeName().getFullyQualifiedName();
                if ("Payload".equals(typeName) || typeName.endsWith(".Payload")) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean isLikelyStateEnumType(Expression expr) {
        if (expr == null) {
            return false;
        }
        ITypeBinding binding = expr.resolveTypeBinding();
        if (binding == null) {
            return false;
        }
        if (!binding.isEnum()) {
            return false;
        }
        String simple = binding.getName();
        return simple != null && simple.toLowerCase(java.util.Locale.ROOT).contains("state");
    }

    private String getSimpleNameString(Expression expr) {
        if (expr instanceof QualifiedName qn) {
            return qn.getName().getIdentifier();
        } else if (expr instanceof FieldAccess fa) {
            return fa.getName().getIdentifier();
        } else if (expr instanceof StringLiteral sl) {
            return sl.getLiteralValue();
        }
        return expr.toString();
    }

    private String extractEventFromMessageBuilder(Expression expr) {
        if (expr == null) {
            return null;
        }
        Expression sharedPeel = MessageCarrierSupport.peelEventFromMessageExpr(expr);
        if (sharedPeel != null && sharedPeel != expr) {
            String fromShared = extractEventFromMessageBuilder(sharedPeel);
            if (fromShared != null) {
                return fromShared;
            }
            String resolved = constantResolver.resolve(sharedPeel, context);
            if (resolved != null) {
                return resolved;
            }
            return sharedPeel.toString();
        }

        if (expr instanceof CastExpression ce) {
            return extractEventFromMessageBuilder(ce.getExpression());
        }

        if (expr instanceof SimpleName sn) {
            String varName = sn.getIdentifier();
            MethodDeclaration enclosingMethod = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(expr);
            if (enclosingMethod != null) {
                // Find variable declaration
                final Expression[] initializer = new Expression[1];
                enclosingMethod.accept(new ASTVisitor() {
                    @Override
                    public boolean visit(VariableDeclarationFragment node) {
                        if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
                            initializer[0] = node.getInitializer();
                        }
                        return super.visit(node);
                    }
                    @Override
                    public boolean visit(Assignment node) {
                        if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
                            initializer[0] = node.getRightHandSide();
                        }
                        return super.visit(node);
                    }
                });
                if (initializer[0] != null) {
                    return extractEventFromMessageBuilder(initializer[0]); // recursive
                }
                
                // If it has NO initializer, it might be a method parameter!
                for (Object paramObj : enclosingMethod.parameters()) {
                    if (paramObj instanceof SingleVariableDeclaration svd) {
                        if (svd.getName().getIdentifier().equals(varName)) {
                            return varName;
                        }
                    }
                }
            }
        }

        if (expr instanceof ClassInstanceCreation cic) {
            return cic.getType().toString();
        }

        if (!(expr instanceof MethodInvocation mi)) return null;

        Expression factoryPayload = ReactiveExpressionSupport.peelFactoryPayloadExpression(mi);
        if (factoryPayload != null) {
            String extracted = extractEventFromMessageBuilder(factoryPayload);
            if (extracted != null) {
                return extracted;
            }
            String resolved = constantResolver.resolve(factoryPayload, context);
            if (resolved != null) {
                return resolved;
            }
            Expression payloadExpr = factoryPayload;
            if (payloadExpr instanceof CastExpression ce) {
                payloadExpr = ce.getExpression();
            }
            if (payloadExpr instanceof SimpleName sn) {
                String traced = extractEventFromMessageBuilder(payloadExpr);
                if (traced != null) {
                    return traced;
                }
                return sn.getIdentifier();
            }
            return payloadExpr.toString();
        }

        MethodInvocation current = mi;
        while (current != null) {
            if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
                // If the event is obtained by calling a method on a provider/supplier parameter, 
                // return the provider's name so the call-graph engine can trace the lambda argument
                String traced = extractEventFromMessageBuilder(sn);
                return traced != null ? traced : sn.getIdentifier();
            }
            
            Expression receiver = current.getExpression();
            if (receiver instanceof MethodInvocation nextMi) {
                current = nextMi;
            } else {
                current = null;
            }
        }
        return null;
    }

    private AbstractTypeDeclaration findEnclosingAbstractType(ASTNode node) {
        ASTNode parent = node.getParent();
        while (parent != null && !(parent instanceof AbstractTypeDeclaration)) {
            parent = parent.getParent();
        }
        return (AbstractTypeDeclaration) parent;
    }

    private List<FieldDeclaration> getFieldDeclarations(AbstractTypeDeclaration type) {
        if (type instanceof TypeDeclaration td) {
            return List.of(td.getFields());
        }
        if (type instanceof RecordDeclaration rd) {
            return List.of(rd.getFields());
        }
        return List.of();
    }

    private Type lookupMemberType(AbstractTypeDeclaration enclosingType, String memberName) {
        if (enclosingType == null || memberName == null) {
            return null;
        }
        for (FieldDeclaration fd : getFieldDeclarations(enclosingType)) {
            for (Object fragObj : fd.fragments()) {
                if (fragObj instanceof VariableDeclarationFragment fragment
                        && fragment.getName().getIdentifier().equals(memberName)) {
                    return fd.getType();
                }
            }
        }
        if (enclosingType instanceof RecordDeclaration rd) {
            for (Object compObj : rd.recordComponents()) {
                if (compObj instanceof SingleVariableDeclaration svd
                        && svd.getName().getIdentifier().equals(memberName)) {
                    return svd.getType();
                }
            }
        }
        return null;
    }

    private String[] resolveStateMachineTypeArguments(MethodInvocation node) {
        Expression receiver = node.getExpression();
        
        // Trace back the receiver if it's a builder chain
        while (receiver instanceof MethodInvocation mi) {
            if (mi.getName().getIdentifier().equals("sendEvent")) {
                receiver = mi.getExpression();
                break;
            }
            receiver = mi.getExpression();
        }
        
        // Also if the node itself is sendEvent, receiver is already correct
        if (node.getName().getIdentifier().equals("sendEvent")) {
            receiver = node.getExpression();
        }

        if (receiver == null) return new String[]{null, null};

        ITypeBinding binding = receiver.resolveTypeBinding();
        if (binding != null) {
            ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>());
            if (smBinding != null) {
                ITypeBinding[] typeArgs = smBinding.getTypeArguments();
                if (typeArgs.length >= 2) {
                    return new String[]{
                            concreteStateMachineTypeArgFqn(typeArgs[0]),
                            concreteStateMachineTypeArgFqn(typeArgs[1])};
                }
            }
        }

        // Fallback: search enclosing class/method for variable declaration
        Type declType = findReceiverTypeManually(receiver, node);
        if (declType instanceof ParameterizedType pt) {
            List<?> typeArgs = pt.typeArguments();
            if (typeArgs.size() >= 2) {
                CompilationUnit cu = (CompilationUnit) node.getRoot();
                String stateType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(0), cu);
                String eventType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(1), cu);
                
                if (stateType != null && stateType.length() == 1) {
                    stateType = resolveGenericTypeVariable(stateType, node, cu);
                }
                if (eventType != null && eventType.length() == 1) {
                    eventType = resolveGenericTypeVariable(eventType, node, cu);
                }
                // Unresolved class type variables (S/E) are not concrete machine types.
                if (isUnresolvedTypeVariableName(stateType)) {
                    stateType = null;
                }
                if (isUnresolvedTypeVariableName(eventType)) {
                    eventType = null;
                }
                
                return new String[]{stateType, eventType};
            }
        }
        return new String[]{null, null};
    }

    private String resolveGenericTypeVariable(String typeVarName, ASTNode node, CompilationUnit cu) {
        AbstractTypeDeclaration enclosingClass = findEnclosingAbstractType(node);
        if (enclosingClass == null) return typeVarName;
        String className = context.getFqn(enclosingClass);
        if (className == null) return typeVarName;

        List<String> impls = context.getImplementations(className);
        if (impls == null || impls.isEmpty()) return typeVarName;

        LinkedHashSet<String> concreteCandidates = new LinkedHashSet<>();
        for (String implFqn : impls) {
            org.eclipse.jdt.core.dom.AbstractTypeDeclaration implNode = context.getAbstractTypeDeclaration(implFqn);
            if (implNode instanceof TypeDeclaration td) {
                Type superclassType = td.getSuperclassType();
                if (superclassType instanceof ParameterizedType pt) {
                    Type baseType = pt.getType();
                    String baseName = typeResolver.resolveTypeToFqn(baseType, (CompilationUnit) implNode.getRoot());
                    if (className.equals(baseName) || className.endsWith("." + baseName)) {
                        int typeIndex = -1;
                        List<?> typeParams = enclosingClass instanceof TypeDeclaration typeDecl
                                ? typeDecl.typeParameters()
                                : enclosingClass instanceof RecordDeclaration recordDecl
                                ? recordDecl.typeParameters()
                                : List.of();
                        for (int i = 0; i < typeParams.size(); i++) {
                            TypeParameter tp = (TypeParameter) typeParams.get(i);
                            if (tp.getName().getIdentifier().equals(typeVarName)) {
                                typeIndex = i;
                                break;
                            }
                        }
                        if (typeIndex >= 0 && typeIndex < pt.typeArguments().size()) {
                            Type concreteType = (Type) pt.typeArguments().get(typeIndex);
                            String resolved = typeResolver.resolveTypeToFqn(
                                    concreteType, (CompilationUnit) implNode.getRoot());
                            if (resolved != null && !isUnresolvedTypeVariableName(resolved)) {
                                concreteCandidates.add(resolved);
                            }
                        }
                    }
                }
            }
        }
        // Only stamp a concrete type when every subclass agrees — otherwise leave unbound so
        // call-chain rebinding can pick the chain-scoped substitution.
        if (concreteCandidates.size() == 1) {
            return concreteCandidates.iterator().next();
        }
        return typeVarName;
    }

    /**
     * Binding-derived {@code StateMachine<S,E>} type arguments that are still unbound type variables
     * (or wildcards) are not concrete machine enum types — leave them absent so routing can use
     * {@code polymorphicEvents} / shared-infrastructure evidence instead.
     */
    private static String concreteStateMachineTypeArgFqn(ITypeBinding typeArg) {
        if (typeArg == null) {
            return null;
        }
        if (typeArg.isTypeVariable() || typeArg.isCapture() || typeArg.isWildcardType()) {
            return null;
        }
        String qualified = typeArg.getQualifiedName();
        if (isUnresolvedTypeVariableName(qualified)) {
            return null;
        }
        return qualified;
    }

    private static boolean isUnresolvedTypeVariableName(String typeName) {
        if (typeName == null || typeName.isBlank()) {
            return false;
        }
        // JDT often reports unbound parameters as a single uppercase letter ("S", "E").
        return typeName.length() == 1 && Character.isUpperCase(typeName.charAt(0));
    }

    private ITypeBinding findStateMachineSupertype(ITypeBinding binding, java.util.Set<String> visited) {
        ITypeBinding current = binding;
        while (current != null) {
            String erasureName = current.getErasure().getQualifiedName();
            if (erasureName != null && !visited.add(erasureName)) {
                return null;
            }
            if ("org.springframework.statemachine.StateMachine".equals(erasureName)) {
                return current;
            }
            for (ITypeBinding iface : current.getInterfaces()) {
                ITypeBinding res = findStateMachineSupertype(iface, visited);
                if (res != null) return res;
            }
            current = current.getSuperclass();
        }
        return null;
    }

    private Type findReceiverTypeManually(Expression receiver, MethodInvocation node) {
        if (!(receiver instanceof SimpleName sn)) return null;
        String varName = sn.getIdentifier();

        MethodDeclaration enclosingMethod = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
        if (enclosingMethod != null) {
            // Check params
            for (Object paramObj : enclosingMethod.parameters()) {
                if (paramObj instanceof SingleVariableDeclaration svd) {
                    if (svd.getName().getIdentifier().equals(varName)) {
                        return svd.getType();
                    }
                }
            }
            // Check local variables
            if (enclosingMethod.getBody() != null) {
                Type[] found = new Type[1];
                enclosingMethod.getBody().accept(new ASTVisitor() {
                    @Override
                    public boolean visit(VariableDeclarationStatement vds) {
                        for (Object fragObj : vds.fragments()) {
                            if (fragObj instanceof VariableDeclarationFragment fragment) {
                                if (fragment.getName().getIdentifier().equals(varName)) {
                                    found[0] = vds.getType();
                                }
                            }
                        }
                        return super.visit(vds);
                    }
                });
                if (found[0] != null) return found[0];
            }
        }

        // Check fields and record components
        AbstractTypeDeclaration enclosingType = findEnclosingAbstractType(node);
        if (enclosingType != null) {
            Type memberType = lookupMemberType(enclosingType, varName);
            if (memberType != null) {
                return memberType;
            }
        }

        return null;
    }

    private static class AssignmentDag {
        final java.util.Map<String, List<Expression>> assignments = new java.util.HashMap<>();
        final java.util.Set<SingleVariableDeclaration> parameters = new java.util.HashSet<>();

        AssignmentDag(MethodDeclaration method) {
            if (method == null) return;
            for (Object paramObj : method.parameters()) {
                if (paramObj instanceof SingleVariableDeclaration svd) {
                    parameters.add(svd);
                }
            }
            if (method.getBody() != null) {
                method.getBody().accept(new ASTVisitor() {
                    @Override
                    public boolean visit(VariableDeclarationFragment fragment) {
                        String varName = fragment.getName().getIdentifier();
                        if (fragment.getInitializer() != null) {
                            assignments.computeIfAbsent(varName, k -> new ArrayList<>()).add(fragment.getInitializer());
                        }
                        return super.visit(fragment);
                    }

                    @Override
                    public boolean visit(Assignment assignment) {
                        if (assignment.getLeftHandSide() instanceof SimpleName sn) {
                            String varName = sn.getIdentifier();
                            assignments.computeIfAbsent(varName, k -> new ArrayList<>()).add(assignment.getRightHandSide());
                        }
                        return super.visit(assignment);
                    }
                });
            }
        }

        boolean isExternal(Expression expr, java.util.Set<String> visited) {
            if (expr == null) return false;
            Expression peeled = expr;
            while (peeled instanceof ParenthesizedExpression pe) {
                peeled = pe.getExpression();
            }
            while (peeled instanceof CastExpression ce) {
                peeled = ce.getExpression();
            }

            if (peeled instanceof SimpleName sn) {
                String varName = sn.getIdentifier();
                for (SingleVariableDeclaration svd : parameters) {
                    if (svd.getName().getIdentifier().equals(varName)) {
                        return isAnnotatedAsExternal(svd);
                    }
                }
                if (assignments.containsKey(varName)) {
                    if (!visited.add(varName)) {
                        return false;
                    }
                    List<Expression> defs = assignments.get(varName);
                    for (Expression def : defs) {
                        if (isExternal(def, visited)) {
                            return true;
                        }
                    }
                }
            } else if (peeled instanceof MethodInvocation mi) {
                if (mi.getExpression() != null && isExternal(mi.getExpression(), visited)) {
                    return true;
                }
                for (Object arg : mi.arguments()) {
                    if (isExternal((Expression) arg, visited)) {
                        return true;
                    }
                }
            }
            return false;
        }

        private boolean isAnnotatedAsExternal(SingleVariableDeclaration svd) {
            for (Object modifier : svd.modifiers()) {
                if (modifier instanceof Annotation annotation) {
                    String typeName = annotation.getTypeName().getFullyQualifiedName();
                    if (typeName.endsWith("PathVariable") || typeName.endsWith("RequestBody") || typeName.endsWith("RequestParam")
                        || typeName.endsWith("PathParam") || typeName.endsWith("QueryParam")) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
}
