package click.kamil.springstatemachineexporter.analysis.flow.eventid;

import click.kamil.springstatemachineexporter.analysis.flow.CallSiteArgBinder;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.AllocationFactPropagator;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.AllocationSite;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.InstanceofNarrower;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.ReceiverFacts;
import click.kamil.springstatemachineexporter.analysis.flow.alloc.VirtualAccessorConstResolver;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.pipeline.EventArgumentSelector;
import click.kamil.springstatemachineexporter.analysis.pipeline.IdentityStringTransformSupport;
import click.kamil.springstatemachineexporter.analysis.pipeline.LibraryUnwrapRegistry;
import click.kamil.springstatemachineexporter.analysis.pipeline.MessageCarrierSupport;
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
import click.kamil.springstatemachineexporter.analysis.pipeline.SwitchConstSupport;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.FunctionalInterfaceTypes;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.ConditionalExpression;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionMethodReference;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.SwitchCase;
import org.eclipse.jdt.core.dom.SwitchExpression;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeMethodReference;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.YieldStatement;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Unified peeler: sendEvent/valueOf/helper/get* expressions → unique compile-time event constant
 * when allocation (or concrete declared type) proves a single accessor override.
 * Also forwards string carriers across call edges so {@code valueOf(code)} can resolve when
 * the caller passed {@code e.getA()}.
 * <p>
 * Intentionally fail-closed forever for: multi-concrete Flux/sendEvents fan-out, opaque external
 * validate wrappers, Mystery/runtime/IO carriers, TX {@code afterCommit} / {@code @Async} handoffs,
 * and unbound request-param {@code valueOf}.
 */
public final class EventIdentityResolver {

    private static final int MAX_DEPTH = 10;
    private static final int MAX_LOCAL_HOPS = 6;

    /** Single-arg fire sites treated like {@code sendEvent} for path recovery. */
    private static final Set<String> SEND_LIKE_NAMES = Set.of(
            "sendEvent",
            "fire",
            "fireEvent",
            "dispatchEvent",
            "send",
            "sendEvents",
            "sendEventCollect",
            "sendEventMono",
            "trigger");

    /** Compact 0-arg identity accessor names; still gated by unique compile-time body constant. */
    private static final Set<String> COMPACT_ACCESSOR_NAMES = Set.of(
            "type", "event", "code", "a", "id");

    private final CodebaseContext context;
    private final AllocationFactPropagator allocationFactPropagator;
    private final VirtualAccessorConstResolver virtualAccessorConstResolver;
    private final VariableTracer variableTracer;
    private final ConstantResolver constantResolver;

    /** Path-scoped result cache: expression identity + path key → proven constant or miss marker. */
    private final Map<String, List<String>> resolveCache = new HashMap<>();

    public EventIdentityResolver(
            CodebaseContext context,
            AllocationFactPropagator allocationFactPropagator,
            VirtualAccessorConstResolver virtualAccessorConstResolver,
            VariableTracer variableTracer,
            ConstantResolver constantResolver) {
        this.context = context;
        this.allocationFactPropagator = allocationFactPropagator;
        this.virtualAccessorConstResolver = virtualAccessorConstResolver;
        this.variableTracer = variableTracer;
        this.constantResolver = constantResolver;
    }

    public void clearSessionCache() {
        resolveCache.clear();
    }

    /**
     * @return singleton list with proven constant, or empty when unknown / ambiguous
     */
    public List<String> resolveUnique(
            Expression expression,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        return resolveUnique(expression, methodPath, callGraph, 0, new LinkedHashSet<>());
    }

    /**
     * Hint-gated multi-member expand for unbound dynamic strings.
     * Thin wrapper over {@link #resolveExpandConsult}; returns concrete members only.
     */
    public List<String> resolveExpanded(
            Expression expression,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        return resolveExpandConsult(expression, methodPath, callGraph).members();
    }

    /**
     * Shared unique-or-expand consult for ACE and future poly publishers.
     * <ol>
     *   <li>If {@link #resolveUnique} proves one constant → {@link ExpandConsult.Source#UNIQUE}.</li>
     *   <li>Else if expr-local or helper-return expand hint licenses an enum type →
     *       {@link ExpandConsult.Source#ENUM_EXPAND} with all concrete members.</li>
     *   <li>Else {@link ExpandConsult.Source#EMPTY}. Never returns analysis tokens.</li>
     * </ol>
     * Poly publishers must use {@link ExpandConsult#adoptableAsPolyExpand()} (or
     * {@link EventIdentityPolyPublish#adoptExpandOrEmpty}) — never publish UNIQUE from consult.
     */
    public ExpandConsult resolveExpandConsult(
            Expression expression,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        if (expression == null) {
            return ExpandConsult.empty();
        }
        List<String> unique = resolveUnique(expression, methodPath, callGraph);
        if (unique.size() == 1) {
            String enumType = inferExpandEnumType(expression);
            List<String> qualified = qualifyMembers(unique, enumType);
            return ExpandConsult.unique(qualified, enumType);
        }
        if (expressionHasExpandHintDeep(expression)) {
            String enumType = inferExpandEnumType(expression);
            if (enumType != null && !enumType.isBlank()) {
                List<String> members = expandDeclaredEnumMembers(enumType);
                if (!members.isEmpty()) {
                    return ExpandConsult.enumExpand(members, enumType);
                }
            }
        }
        ExpandConsult fromHelper = expandViaHelperPath(expression, methodPath);
        if (!fromHelper.isEmpty()) {
            return fromHelper;
        }
        return ExpandConsult.empty();
    }

    /**
     * Peel expression to an {@link AccessProof} when a virtual accessor + receiver facts are proven.
     */
    public AccessProof proveAccess(
            Expression expression,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        if (expression == null) {
            return null;
        }
        Expression peeled = peel(expression);
        MethodInvocation accessor = findVirtualAccessorInvocation(peeled);
        if (accessor == null) {
            return null;
        }
        MethodDeclaration enclosing = findEnclosingMethod(accessor);
        ReceiverFacts facts = allocationFactPropagator == null
                ? ReceiverFacts.empty()
                : allocationFactPropagator.factsForAccessorReceiver(accessor, methodPath, callGraph);
        if (facts.isEmpty()) {
            facts = factsFromConcreteReceiverType(
                    accessor, enclosing, methodPath, callGraph, 0, new LinkedHashSet<>());
        }
        AccessProof proof = new AccessProof(
                facts, accessor.getName().getIdentifier(), accessor, enclosing);
        return proof.isValid() ? proof : null;
    }

    /**
     * Path recovery: scan method frames for candidate exprs and resolve with the same peels.
     */
    public List<String> recoverFromPath(
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        if (methodPath == null || methodPath.size() < 2) {
            return List.of();
        }
        for (int i = methodPath.size() - 2; i >= 0; i--) {
            String methodFqn = methodPath.get(i);
            MethodDeclaration md = findMethodDeclaration(methodFqn);
            if (md == null || md.getBody() == null) {
                continue;
            }
            List<Expression> preferred = new ArrayList<>();
            List<Expression> fallback = new ArrayList<>();
            md.getBody().accept(new ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    String name = node.getName().getIdentifier();
                    // Only fish expressions tied to valueOf / send-like / valueOf-helpers.
                    // Do not harvest bare getType() — that over-resolves through opaque wrappers.
                    if ("valueOf".equals(name)
                            && (node.arguments().size() == 1 || node.arguments().size() == 2)
                            && node.arguments().get(node.arguments().size() - 1) instanceof Expression arg) {
                        preferred.add(node);
                        preferred.add(arg);
                        return super.visit(node);
                    }
                    if (isSendLikeName(name) && !node.arguments().isEmpty()) {
                        EventArgumentSelector.selectEventArgument(node).ifPresent(preferred::add);
                        return super.visit(node);
                    }
                    if (node.arguments().size() == 1 && looksLikeValueOfHelper(node)) {
                        preferred.add(node);
                    }
                    return super.visit(node);
                }
            });
            List<String> fromPreferred = resolveUniqueAgreeing(preferred, methodPath, callGraph);
            if (fromPreferred.size() == 1) {
                return fromPreferred;
            }
            List<String> fromFallback = resolveUniqueAgreeing(fallback, methodPath, callGraph);
            if (fromFallback.size() == 1) {
                return fromFallback;
            }
        }
        return List.of();
    }

    /**
     * Resolve preferred exprs; succeed only when every unique proof agrees on the same constant.
     * Multiple different unique proofs → empty (fail-closed, no first-wins).
     * Bare {@code PAY} and qualified {@code OrderEvent.PAY} count as the same proof.
     */
    private List<String> resolveUniqueAgreeing(
            List<Expression> exprs,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        // simpleName → preferred (qualified) form
        java.util.LinkedHashMap<String, String> proven = new java.util.LinkedHashMap<>();
        for (Expression expr : exprs) {
            List<String> unique = resolveUnique(expr, methodPath, callGraph);
            if (unique.size() != 1) {
                continue;
            }
            String constant = unique.get(0);
            String simple = EventIdentityNormalize.simpleName(constant);
            if (simple == null) {
                continue;
            }
            String existing = proven.get(simple);
            if (existing == null) {
                proven.put(simple, constant);
            } else {
                proven.put(simple, EventIdentityNormalize.preferQualified(existing, constant, null));
            }
            if (proven.size() > 1) {
                return List.of();
            }
        }
        return proven.size() == 1 ? List.copyOf(proven.values()) : List.of();
    }

    private List<String> resolveUnique(
            Expression expression,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        if (expression == null || depth > MAX_DEPTH) {
            return List.of();
        }
        String cacheKey = cacheKey(expression, methodPath, depth);
        if (cacheKey != null) {
            List<String> cached = resolveCache.get(cacheKey);
            if (cached != null) {
                return cached;
            }
        }

        List<String> result = resolveUniqueUncached(expression, methodPath, callGraph, depth, visited);
        if (cacheKey != null && (result.size() == 1 || result.isEmpty())) {
            resolveCache.put(cacheKey, result);
        }
        return result;
    }

    private List<String> resolveUniqueUncached(
            Expression expression,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        Expression peeled = peelIdentityString(peel(expression));

        // 0. MessageBuilder / GenericMessage / allowlisted setHeader before other peels
        List<String> fromMessage = resolveFromMessageCarrier(peeled, methodPath, callGraph, depth, visited);
        if (fromMessage.size() == 1) {
            return fromMessage;
        }

        // 0b. Literal lambda / Supplier body
        if (peeled instanceof LambdaExpression lambda) {
            Expression body = ReactiveExpressionSupport.lambdaBodyExpression(lambda);
            if (body != null) {
                return resolveUnique(body, methodPath, callGraph, depth + 1, visited);
            }
            return List.of();
        }

        // 1. Compile-time enum / qualified constant
        if (constantResolver != null) {
            String direct = constantResolver.resolve(peeled, context);
            String normalized = VirtualAccessorConstResolver.normalizeConstant(direct);
            if (normalized != null && looksLikeEnumConstant(normalized)) {
                return List.of(normalized);
            }
        }
        if (peeled instanceof QualifiedName qn) {
            String id = qn.getName().getIdentifier();
            if (id != null && id.matches("[A-Z_][A-Z0-9_]*")) {
                return List.of(id);
            }
        }

        // 2. Virtual accessor call (via AccessProof when possible)
        if (peeled instanceof MethodInvocation mi && isVirtualAccessorName(mi.getName().getIdentifier())
                && mi.getExpression() != null
                && mi.arguments().isEmpty()) {
            return resolveFromAccessor(mi, methodPath, callGraph, depth, visited);
        }

        // 2a. Supplier/Callable.get() when receiver binds a unique lambda on the path
        if (peeled instanceof MethodInvocation getMi
                && getMi.arguments().isEmpty()
                && getMi.getExpression() != null
                && FunctionalInterfaceTypes.isSamMethodName(getMi.getName().getIdentifier())
                && !isIdentityContainerReceiver(getMi.getExpression())) {
            List<String> fromFunctional = resolveFromFunctionalGet(
                    getMi, methodPath, callGraph, depth, visited);
            if (fromFunctional.size() == 1) {
                return fromFunctional;
            }
        }

        // 2a2. headers.get("event") / getHeaders().get("smEvent")
        if (peeled instanceof MethodInvocation headerGet
                && "get".equals(headerGet.getName().getIdentifier())
                && headerGet.arguments().size() == 1) {
            List<String> fromHeaderGet = resolveFromHeadersGet(
                    headerGet, methodPath, callGraph, depth, visited);
            if (fromHeaderGet.size() == 1) {
                return fromHeaderGet;
            }
        }

        // 2b. Enum.name() peel: valueOf(e.getType().name()) → resolve e.getType()
        if (peeled instanceof MethodInvocation nameMi
                && "name".equals(nameMi.getName().getIdentifier())
                && nameMi.arguments().isEmpty()
                && nameMi.getExpression() != null) {
            return resolveUnique(nameMi.getExpression(), methodPath, callGraph, depth + 1, visited);
        }

        // 2c. Ternary: both arms must prove the same unique constant
        if (peeled instanceof ConditionalExpression cond) {
            List<String> thenArm = resolveUnique(cond.getThenExpression(), methodPath, callGraph, depth + 1, visited);
            List<String> elseArm = resolveUnique(cond.getElseExpression(), methodPath, callGraph, depth + 1, visited);
            if (thenArm.size() == 1 && elseArm.size() == 1 && thenArm.get(0).equals(elseArm.get(0))) {
                return thenArm;
            }
            return List.of();
        }

        // 2d. Switch expression: selector maps to unique arm, or all arms agree
        if (peeled instanceof SwitchExpression se) {
            return resolveFromSwitchExpression(se, methodPath, callGraph, depth, visited);
        }

        // 2e. Optional.orElse(CONST) — both present side and else must agree
        if (peeled instanceof MethodInvocation orElseMi
                && "orElse".equals(orElseMi.getName().getIdentifier())
                && orElseMi.arguments().size() == 1
                && orElseMi.getExpression() != null
                && orElseMi.arguments().get(0) instanceof Expression elseArg) {
            List<String> elseArm = resolveUnique(elseArg, methodPath, callGraph, depth + 1, visited);
            Expression present = peelOptionalPresent(orElseMi.getExpression());
            if (present != null && elseArm.size() == 1) {
                List<String> presentArm = resolveUnique(present, methodPath, callGraph, depth + 1, visited);
                if (presentArm.size() == 1 && presentArm.get(0).equals(elseArm.get(0))) {
                    return presentArm;
                }
            }
            return List.of();
        }

        // 3. Enum.valueOf(expr) or Enum.valueOf(Class, String)
        if (peeled instanceof MethodInvocation valueOfMi
                && "valueOf".equals(valueOfMi.getName().getIdentifier())) {
            if (valueOfMi.arguments().size() == 1
                    && valueOfMi.arguments().get(0) instanceof Expression arg) {
                return resolveUnique(arg, methodPath, callGraph, depth + 1, visited);
            }
            if (valueOfMi.arguments().size() == 2
                    && valueOfMi.arguments().get(1) instanceof Expression arg) {
                return resolveUnique(arg, methodPath, callGraph, depth + 1, visited);
            }
        }

        // 4. Helpers: valueOf(param), passthrough return param, or unique const-map (switch/if-equals)
        if (peeled instanceof MethodInvocation helperMi && helperMi.arguments().size() == 1
                && !"valueOf".equals(helperMi.getName().getIdentifier())
                && !isVirtualAccessorName(helperMi.getName().getIdentifier())) {
            List<String> fromHelper = resolveFromValueOfHelper(helperMi, methodPath, callGraph, depth, visited);
            if (!fromHelper.isEmpty()) {
                return fromHelper;
            }
            List<String> fromPass = resolveFromPassthroughHelper(helperMi, methodPath, callGraph, depth, visited);
            if (!fromPass.isEmpty()) {
                return fromPass;
            }
            List<String> fromMap = resolveFromConstMapHelper(helperMi, methodPath, callGraph, depth, visited);
            if (!fromMap.isEmpty()) {
                return fromMap;
            }
        }

        // 4b. Public final identity field: valueOf(e.a) — AST FieldAccess or reparsed QualifiedName
        if (peeled instanceof FieldAccess fa && fa.getExpression() != null) {
            List<String> fromField = resolveFromIdentityFieldAccess(
                    fa.getExpression(), fa.getName().getIdentifier(), methodPath, callGraph, depth, visited);
            if (fromField.size() == 1) {
                return fromField;
            }
        }
        if (peeled instanceof QualifiedName qn
                && qn.getQualifier() instanceof SimpleName
                && !looksLikeEnumConstant(qn.getName().getIdentifier())) {
            List<String> fromField = resolveFromIdentityFieldAccess(
                    qn.getQualifier(), qn.getName().getIdentifier(), methodPath, callGraph, depth, visited);
            if (fromField.size() == 1) {
                return fromField;
            }
        }

        // 5. SimpleName → VariableTracer / local init / interprocedural string param
        if (peeled instanceof SimpleName) {
            if (variableTracer != null) {
                for (Expression def : variableTracer.traceVariableAll(peeled)) {
                    Expression defPeeled = peelIdentityString(peel(def));
                    if (defPeeled == null || defPeeled == peeled) {
                        continue;
                    }
                    String key = "sn:" + System.identityHashCode(defPeeled);
                    if (!visited.add(key)) {
                        continue;
                    }
                    List<String> nested = resolveUnique(defPeeled, methodPath, callGraph, depth + 1, visited);
                    if (nested.size() == 1) {
                        return nested;
                    }
                }
            }
            MethodDeclaration enclosing = findEnclosingMethod(peeled);
            if (enclosing != null && enclosing.getBody() != null
                    && peeled instanceof SimpleName sn) {
                Expression init = findLocalInitializer(enclosing, sn.getIdentifier());
                if (init != null) {
                    String key = "init:" + sn.getIdentifier() + "@" + System.identityHashCode(enclosing);
                    if (visited.add(key)) {
                        List<String> nested = resolveUnique(init, methodPath, callGraph, depth + 1, visited);
                        if (nested.size() == 1) {
                            return nested;
                        }
                    }
                }
                // Interprocedural: SimpleName is a callee param → caller arg AST → recurse
                List<String> fromCaller = resolveFromCallerStringArgument(
                        sn, enclosing, methodPath, callGraph, depth, visited);
                if (fromCaller.size() == 1) {
                    return fromCaller;
                }
            }
        }

        return List.of();
    }

    /**
     * When {@code code} is a parameter of the enclosing method, locate the unique caller
     * argument on the path ({@code route(e.getA())}) and resolve that expression.
     */
    private List<String> resolveFromCallerStringArgument(
            SimpleName paramName,
            MethodDeclaration enclosing,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        if (paramName == null || enclosing == null || methodPath == null || callGraph == null) {
            return List.of();
        }
        int paramIndex = parameterIndex(enclosing, paramName.getIdentifier());
        if (paramIndex < 0) {
            return List.of();
        }
        String calleeFqn = methodFqn(enclosing);
        if (calleeFqn == null) {
            return List.of();
        }
        String visitKey = "strParam:" + calleeFqn + "#" + paramIndex;
        if (!visited.add(visitKey)) {
            return List.of();
        }
        int calleeIdx = methodPath.indexOf(calleeFqn);
        if (calleeIdx <= 0) {
            calleeIdx = indexOfMethodOnPath(methodPath, calleeFqn);
        }
        if (calleeIdx <= 0) {
            return List.of();
        }
        String callerMethodFqn = methodPath.get(calleeIdx - 1);
        List<CallEdge> edges = callGraph.get(callerMethodFqn);
        if (edges == null || edges.isEmpty()) {
            return List.of();
        }
        LinkedHashSet<String> proven = new LinkedHashSet<>();
        boolean sawMatchingEdge = false;
        for (CallEdge edge : edges) {
            if (edge == null || edge.getTargetMethod() == null) {
                continue;
            }
            if (!CallSiteArgBinder.methodsMatch(edge.getTargetMethod(), calleeFqn)) {
                continue;
            }
            sawMatchingEdge = true;
            MethodDeclaration callerMd = findMethodDeclaration(callerMethodFqn);
            Expression callerArg = callerMd == null
                    ? null
                    : CallSiteArgBinder.findUniqueArgument(callerMd, edge, paramIndex).orElse(null);
            if (callerArg == null) {
                // Soft miss on one edge — keep scanning; fail closed if none resolve uniquely.
                continue;
            }
            List<String> nested = resolveUnique(callerArg, methodPath, callGraph, depth + 1, visited);
            if (nested.size() == 1) {
                proven.add(nested.get(0));
            } else if (!nested.isEmpty()) {
                return List.of();
            }
            if (proven.size() > 1) {
                return List.of();
            }
        }
        if (!sawMatchingEdge || proven.size() != 1) {
            return List.of();
        }
        return List.copyOf(proven);
    }

    /**
     * Pure passthrough: {@code T f(T x) { return x; }} / assert wrappers — map to caller arg.
     */
    private List<String> resolveFromPassthroughHelper(
            MethodInvocation helperCall,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        String key = "pass:" + System.identityHashCode(helperCall);
        if (!visited.add(key) || helperCall.arguments().isEmpty()) {
            return List.of();
        }
        MethodDeclaration helperMd = resolveHelperMethod(helperCall, methodPath);
        if (helperMd == null) {
            return List.of();
        }
        int paramIdx = MethodInvocationUnwrapper.getReturnedParameterIndex(helperMd);
        if (paramIdx < 0 || paramIdx >= helperCall.arguments().size()) {
            return List.of();
        }
        Expression callerArg = (Expression) helperCall.arguments().get(paramIdx);
        return resolveUnique(callerArg, methodPath, callGraph, depth + 1, visited);
    }

    /**
     * Const-map helpers: switch/if-equals on the string param → enum constants.
     * Succeeds only when the caller arg proves a unique key that maps to one constant.
     */
    private List<String> resolveFromConstMapHelper(
            MethodInvocation helperCall,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        String key = "cmap:" + System.identityHashCode(helperCall);
        if (!visited.add(key) || helperCall.arguments().isEmpty()) {
            return List.of();
        }
        MethodDeclaration helperMd = resolveHelperMethod(helperCall, methodPath);
        if (helperMd == null || helperMd.getBody() == null || helperMd.parameters().isEmpty()) {
            return List.of();
        }
        Expression callerArg = (Expression) helperCall.arguments().get(0);
        List<String> argKeys = resolveUnique(callerArg, methodPath, callGraph, depth + 1, visited);
        if (argKeys.size() != 1) {
            return List.of();
        }
        String keyConst = VirtualAccessorConstResolver.normalizeConstant(argKeys.get(0));
        if (keyConst == null) {
            keyConst = argKeys.get(0);
        }
        if (keyConst.contains(".")) {
            keyConst = keyConst.substring(keyConst.lastIndexOf('.') + 1);
        }
        Map<String, String> caseToConst = extractStringCaseMap(helperMd);
        if (caseToConst.isEmpty()) {
            return List.of();
        }
        String mapped = caseToConst.get(keyConst);
        if (mapped == null) {
            mapped = caseToConst.get(argKeys.get(0));
        }
        if (mapped == null) {
            return List.of();
        }
        String normalized = VirtualAccessorConstResolver.normalizeConstant(mapped);
        if (normalized != null && looksLikeEnumConstant(normalized)) {
            return List.of(normalized);
        }
        return looksLikeEnumConstant(mapped) ? List.of(mapped) : List.of();
    }

    private List<String> resolveFromSwitchExpression(
            SwitchExpression se,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        String key = "swexpr:" + System.identityHashCode(se);
        if (!visited.add(key)) {
            return List.of();
        }
        // All arms (including default) must agree for a path-insensitive unique proof.
        // Throw-only / missing payloads are opaque — never unique-agree on a partial set.
        if (!SwitchConstSupport.hasOpaqueArm(se)) {
            LinkedHashSet<String> allArms = new LinkedHashSet<>();
            for (Expression armExpr : SwitchConstSupport.armPayloadExpressions(se)) {
                String constVal = resolveReturnConstant(armExpr);
                if (constVal != null) {
                    allArms.add(constVal);
                } else {
                    allArms.add("__opaque__" + System.identityHashCode(armExpr));
                }
            }
            if (allArms.size() == 1) {
                String only = allArms.iterator().next();
                if (only.startsWith("__opaque__")) {
                    return List.of();
                }
                String normalized = VirtualAccessorConstResolver.normalizeConstant(only);
                if (normalized != null && looksLikeEnumConstant(normalized)) {
                    return List.of(normalized);
                }
                return looksLikeEnumConstant(only) ? List.of(only) : List.of();
            }
        }
        Map<String, String> caseToConst = new HashMap<>();
        ingestSwitchCases(se.statements(), caseToConst);
        if (caseToConst.isEmpty()) {
            return List.of();
        }
        List<String> selectorKeys = resolveUnique(se.getExpression(), methodPath, callGraph, depth + 1, visited);
        if (selectorKeys.size() != 1) {
            return List.of();
        }
        String keyConst = VirtualAccessorConstResolver.normalizeConstant(selectorKeys.get(0));
        if (keyConst == null) {
            keyConst = selectorKeys.get(0);
        }
        if (keyConst.contains(".")) {
            keyConst = keyConst.substring(keyConst.lastIndexOf('.') + 1);
        }
        String mapped = caseToConst.get(keyConst);
        if (mapped == null) {
            mapped = caseToConst.get(selectorKeys.get(0));
        }
        if (mapped == null) {
            return List.of();
        }
        String normalized = VirtualAccessorConstResolver.normalizeConstant(mapped);
        if (normalized != null && looksLikeEnumConstant(normalized)) {
            return List.of(normalized);
        }
        return looksLikeEnumConstant(mapped) ? List.of(mapped) : List.of();
    }

    private static Expression peelOptionalPresent(Expression optionalExpr) {
        Expression current = peel(optionalExpr);
        if (current instanceof MethodInvocation mi
                && "of".equals(mi.getName().getIdentifier())
                && mi.arguments().size() == 1
                && mi.arguments().get(0) instanceof Expression arg) {
            String recvText = mi.getExpression() == null ? "" : mi.getExpression().toString();
            if (recvText.endsWith("Optional") || recvText.contains("Optional.")) {
                return peel(arg);
            }
        }
        if (current instanceof MethodInvocation mi
                && "ofNullable".equals(mi.getName().getIdentifier())
                && mi.arguments().size() == 1
                && mi.arguments().get(0) instanceof Expression arg) {
            String recvText = mi.getExpression() == null ? "" : mi.getExpression().toString();
            if (recvText.endsWith("Optional") || recvText.contains("Optional.")) {
                return peel(arg);
            }
        }
        return null;
    }

    private Map<String, String> extractStringCaseMap(MethodDeclaration method) {
        Map<String, String> map = new HashMap<>();
        if (method.getBody() == null) {
            return map;
        }
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(SwitchStatement node) {
                ingestSwitchCases(node.statements(), map);
                return false;
            }

            @Override
            public boolean visit(SwitchExpression node) {
                ingestSwitchCases(node.statements(), map);
                return false;
            }

            @Override
            public boolean visit(IfStatement node) {
                String lit = extractEqualsStringLiteral(node.getExpression());
                if (lit != null && node.getThenStatement() != null) {
                    node.getThenStatement().accept(new ASTVisitor() {
                        @Override
                        public boolean visit(ReturnStatement rs) {
                            String constVal = resolveReturnConstant(rs.getExpression());
                            if (constVal != null) {
                                map.put(lit, constVal);
                            }
                            return false;
                        }
                    });
                }
                return super.visit(node);
            }
        });
        return map;
    }

    private void ingestSwitchCases(List<?> statements, Map<String, String> map) {
        Map<String, Expression> payloads = SwitchConstSupport.stringCasePayloads(
                statements,
                this::resolveSwitchCaseKey);
        for (Map.Entry<String, Expression> entry : payloads.entrySet()) {
            String constVal = resolveReturnConstant(entry.getValue());
            if (constVal != null) {
                map.put(entry.getKey(), constVal);
            }
        }
    }

    private String resolveSwitchCaseKey(Expression caseExpr) {
        if (caseExpr == null || constantResolver == null) {
            return null;
        }
        String resolved = constantResolver.resolve(caseExpr, context);
        if (resolved == null) {
            return null;
        }
        String currentKey = VirtualAccessorConstResolver.normalizeConstant(resolved);
        if (currentKey != null && currentKey.contains(".")) {
            currentKey = currentKey.substring(currentKey.lastIndexOf('.') + 1);
        }
        return currentKey;
    }

    private String resolveReturnConstant(Expression expression) {
        if (expression == null) {
            return null;
        }
        Expression peeled = peel(expression);
        if (peeled instanceof StringLiteral sl) {
            String lit = sl.getLiteralValue();
            return looksLikeEnumConstant(lit) ? lit : null;
        }
        if (constantResolver == null) {
            return null;
        }
        String direct = constantResolver.resolve(peeled, context);
        String normalized = VirtualAccessorConstResolver.normalizeConstant(direct);
        if (normalized != null && looksLikeEnumConstant(normalized)) {
            return normalized;
        }
        if (peeled instanceof QualifiedName qn) {
            String id = qn.getName().getIdentifier();
            if (looksLikeEnumConstant(id)) {
                return id;
            }
        }
        return null;
    }

    private static String extractEqualsStringLiteral(Expression condition) {
        Expression peeled = peel(condition);
        if (peeled instanceof MethodInvocation mi
                && "equals".equals(mi.getName().getIdentifier())
                && mi.arguments().size() == 1) {
            Expression recv = mi.getExpression();
            Expression arg = (Expression) mi.arguments().get(0);
            if (recv instanceof StringLiteral sl) {
                return sl.getLiteralValue();
            }
            if (arg instanceof StringLiteral sl) {
                return sl.getLiteralValue();
            }
        }
        if (peeled instanceof InfixExpression infix
                && InfixExpression.Operator.EQUALS.equals(infix.getOperator())) {
            if (infix.getLeftOperand() instanceof StringLiteral sl) {
                return sl.getLiteralValue();
            }
            if (infix.getRightOperand() instanceof StringLiteral sl) {
                return sl.getLiteralValue();
            }
        }
        return null;
    }

    private List<String> resolveFromAccessor(
            MethodInvocation accessorCall,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        if (allocationFactPropagator == null || virtualAccessorConstResolver == null) {
            return List.of();
        }
        AccessProof proof = proveAccess(accessorCall, methodPath, callGraph);
        if (proof != null && proof.isValid()) {
            List<String> proven = virtualAccessorConstResolver.resolveUniqueConstant(
                    proof.receiverFacts(), proof.accessorName());
            if (proven.size() == 1) {
                return proven;
            }
        }
        // Optional.of(e).get().getA() — peel identity wrappers on the receiver, then re-prove.
        Expression rawRecv = accessorCall.getExpression();
        Expression peeledRecv = peelIdentityString(peel(rawRecv));
        if (peeledRecv != null && peeledRecv != rawRecv) {
            MethodDeclaration enclosing = findEnclosingMethod(accessorCall);
            ReceiverFacts facts = allocationFactPropagator.factsAt(
                    peeledRecv, enclosing, methodPath, callGraph);
            if (facts.isEmpty()) {
                facts = factsFromConcreteReceiverTypeOnExpr(
                        peeledRecv, enclosing, methodPath, callGraph);
            }
            if (!facts.isEmpty()) {
                return virtualAccessorConstResolver.resolveUniqueConstant(
                        facts, accessorCall.getName().getIdentifier());
            }
        }
        return List.of();
    }

    private ReceiverFacts factsFromConcreteReceiverTypeOnExpr(
            Expression receiver,
            MethodDeclaration enclosing,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        if (receiver == null) {
            return ReceiverFacts.empty();
        }
        String typeFqn = resolveExpressionTypeFqn(receiver, enclosing);
        if (typeFqn == null) {
            typeFqn = resolveParamTypeFromAst(receiver, enclosing);
        }
        if (typeFqn == null) {
            return ReceiverFacts.empty();
        }
        return factsFromConcreteTypeName(typeFqn, receiver, enclosing, methodPath, callGraph);
    }

    /**
     * When receiver is typed as a concrete class (not interface/abstract), prove accessor
     * constant from that type alone (covers {@code fire(PayRichEvent e)} without CIC on this hop).
     * Instanceof / pattern narrowing is owned by {@link AllocationFactPropagator}.
     */
    private ReceiverFacts factsFromConcreteReceiverType(
            MethodInvocation accessorCall,
            MethodDeclaration enclosing,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        Expression receiver = accessorCall.getExpression();
        if (receiver == null) {
            return ReceiverFacts.empty();
        }
        String typeFqn = resolveExpressionTypeFqn(receiver, enclosing);
        if (typeFqn == null) {
            typeFqn = resolveParamTypeFromAst(receiver, enclosing);
        }
        if (typeFqn == null) {
            return ReceiverFacts.empty();
        }
        return factsFromConcreteTypeName(typeFqn, receiver, enclosing, methodPath, callGraph);
    }

    private ReceiverFacts factsFromConcreteTypeName(
            String typeFqn,
            Expression receiver,
            MethodDeclaration enclosing,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        TypeDeclaration td = context.getTypeDeclaration(typeFqn);
        if (td == null) {
            String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
            td = context.getTypeDeclaration(simple);
            if (td != null) {
                typeFqn = context.getFqn(td);
            }
        }
        if (td == null || td.isInterface() || Modifier.isAbstract(td.getModifiers())) {
            if (allocationFactPropagator != null) {
                return allocationFactPropagator.factsAt(receiver, enclosing, methodPath, callGraph);
            }
            return ReceiverFacts.empty();
        }
        return ReceiverFacts.of(new AllocationSite(typeFqn));
    }

    private List<String> resolveFromValueOfHelper(
            MethodInvocation helperCall,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        String key = "helper:" + System.identityHashCode(helperCall);
        if (!visited.add(key) || helperCall.arguments().isEmpty()) {
            return List.of();
        }
        MethodDeclaration helperMd = resolveHelperMethod(helperCall, methodPath);
        if (helperMd == null || helperMd.getBody() == null) {
            return List.of();
        }
        Expression valueOfArg = extractUniqueValueOfArgument(helperMd);
        if (valueOfArg == null) {
            return List.of();
        }
        Expression mapped = mapHelperValueOfArgToCallerArg(valueOfArg, helperMd, helperCall);
        if (mapped != null) {
            return resolveUnique(mapped, methodPath, callGraph, depth + 1, visited);
        }
        return List.of();
    }

    /**
     * Map {@code valueOf(x)} inside a helper to the caller argument when {@code x} is the
     * parameter (or a local that uniquely aliases the parameter).
     */
    private Expression mapHelperValueOfArgToCallerArg(
            Expression valueOfArg,
            MethodDeclaration helperMd,
            MethodInvocation helperCall) {
        Expression current = peel(valueOfArg);
        for (int hop = 0; hop < MAX_LOCAL_HOPS && current != null; hop++) {
            if (current instanceof SimpleName sn) {
                int idx = parameterIndex(helperMd, sn.getIdentifier());
                if (idx >= 0 && idx < helperCall.arguments().size()) {
                    return (Expression) helperCall.arguments().get(idx);
                }
                Expression init = findLocalDefinition(helperMd, sn.getIdentifier());
                if (init == null || init == current) {
                    return null;
                }
                current = peel(init);
                continue;
            }
            return null;
        }
        return null;
    }

    /**
     * True when method body uniquely returns {@code Enum.valueOf(x)} (cast/paren peeled;
     * {@code x} may be a local assigned from the param). Multiple returns OK when every
     * valueOf arg aliases the same helper parameter.
     */
    private Expression extractUniqueValueOfArgument(MethodDeclaration method) {
        if (method == null || method.getBody() == null) {
            return null;
        }
        List<Expression> valueOfArgs = new ArrayList<>();
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement node) {
                Expression expr = peel(node.getExpression());
                if (expr instanceof MethodInvocation mi
                        && "valueOf".equals(mi.getName().getIdentifier())
                        && mi.arguments().size() == 1
                        && mi.arguments().get(0) instanceof Expression arg) {
                    valueOfArgs.add(peel(arg));
                } else if (expr instanceof MethodInvocation mi
                        && "valueOf".equals(mi.getName().getIdentifier())
                        && mi.arguments().size() == 2
                        && mi.arguments().get(1) instanceof Expression arg) {
                    valueOfArgs.add(peel(arg));
                }
                return super.visit(node);
            }
        });
        if (valueOfArgs.isEmpty()) {
            return null;
        }
        if (valueOfArgs.size() == 1) {
            return valueOfArgs.get(0);
        }
        String sharedParam = null;
        for (Expression arg : valueOfArgs) {
            String param = resolveToHelperParamName(arg, method);
            if (param == null) {
                return null;
            }
            if (sharedParam == null) {
                sharedParam = param;
            } else if (!sharedParam.equals(param)) {
                return null;
            }
        }
        return valueOfArgs.get(0);
    }

    private String resolveToHelperParamName(Expression valueOfArg, MethodDeclaration helperMd) {
        Expression current = peel(valueOfArg);
        for (int hop = 0; hop < MAX_LOCAL_HOPS && current != null; hop++) {
            if (current instanceof SimpleName sn) {
                int idx = parameterIndex(helperMd, sn.getIdentifier());
                if (idx >= 0) {
                    return sn.getIdentifier();
                }
                Expression init = findLocalDefinition(helperMd, sn.getIdentifier());
                if (init == null || init == current) {
                    return null;
                }
                current = peel(init);
                continue;
            }
            return null;
        }
        return null;
    }

    private List<String> resolveFromIdentityFieldAccess(
            Expression receiver,
            String fieldName,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        if (receiver == null || fieldName == null || fieldName.isBlank() || constantResolver == null) {
            return List.of();
        }
        MethodDeclaration enclosing = findEnclosingMethod(receiver);
        ReceiverFacts facts = allocationFactPropagator == null
                ? ReceiverFacts.empty()
                : allocationFactPropagator.factsAt(receiver, enclosing, methodPath, callGraph);
        AllocationSite unique = facts.uniqueOrNull();
        String typeFqn = unique != null ? unique.typeFqn() : null;
        if (typeFqn == null) {
            typeFqn = resolveExpressionTypeFqn(receiver, enclosing);
        }
        if (typeFqn == null) {
            typeFqn = resolveParamTypeFromAst(receiver, enclosing);
        }
        if (typeFqn == null) {
            return List.of();
        }
        TypeDeclaration td = context.getTypeDeclaration(typeFqn);
        if (td == null) {
            String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
            td = context.getTypeDeclaration(simple);
        }
        if (td == null) {
            return List.of();
        }
        Expression init = findFinalFieldInitializer(td, fieldName);
        if (init == null) {
            return List.of();
        }
        String key = "fieldId:" + context.getFqn(td) + "#" + fieldName;
        if (!visited.add(key)) {
            return List.of();
        }
        return resolveUnique(init, methodPath, callGraph, depth + 1, visited);
    }

    private static String resolveParamTypeFromAst(Expression expression, MethodDeclaration enclosing) {
        if (!(expression instanceof SimpleName sn) || enclosing == null) {
            return null;
        }
        String name = sn.getIdentifier();
        for (Object paramObj : enclosing.parameters()) {
            if (paramObj instanceof SingleVariableDeclaration svd
                    && name.equals(svd.getName().getIdentifier())
                    && svd.getType() != null) {
                String type = svd.getType().toString();
                int generic = type.indexOf('<');
                return generic >= 0 ? type.substring(0, generic) : type;
            }
        }
        return InstanceofNarrower.resolvePatternVariableType(name, enclosing);
    }

    private static Expression findFinalFieldInitializer(TypeDeclaration td, String fieldName) {
        if (td == null || fieldName == null) {
            return null;
        }
        for (FieldDeclaration fd : td.getFields()) {
            if (!Modifier.isFinal(fd.getModifiers())) {
                continue;
            }
            for (Object fragObj : fd.fragments()) {
                if (fragObj instanceof VariableDeclarationFragment frag
                        && fieldName.equals(frag.getName().getIdentifier())
                        && frag.getInitializer() != null) {
                    return frag.getInitializer();
                }
            }
        }
        return null;
    }

    private MethodDeclaration resolveHelperMethod(MethodInvocation call) {
        return resolveHelperMethod(call, null);
    }

    private MethodDeclaration resolveHelperMethod(MethodInvocation call, List<String> methodPath) {
        String methodName = call.getName().getIdentifier();
        TypeDeclaration owner = null;
        if (call.getExpression() == null) {
            owner = findEnclosingType(call);
        } else if (call.getExpression() instanceof SimpleName sn) {
            owner = findEnclosingType(call);
            TypeDeclaration named = context.getTypeDeclaration(sn.getIdentifier());
            if (named != null) {
                owner = named;
            }
        } else if (call.getExpression() instanceof Name name) {
            owner = context.getTypeDeclaration(name.getFullyQualifiedName());
        }
        if (owner == null) {
            owner = findEnclosingType(call);
        }
        if (owner != null) {
            MethodDeclaration md = context.findMethodDeclaration(owner, methodName, true);
            if (md != null) {
                return md;
            }
            String superFqn = context.getSuperclassFqn(owner);
            int hops = 0;
            while (superFqn != null && hops++ < 8) {
                TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
                if (superTd == null) {
                    break;
                }
                md = context.findMethodDeclaration(superTd, methodName, true);
                if (md != null) {
                    return md;
                }
                superFqn = context.getSuperclassFqn(superTd);
            }
        }
        // Detached / string-parsed invocations have no enclosing type — walk the call path.
        if (methodPath != null) {
            for (String methodFqn : methodPath) {
                if (methodFqn == null || !methodFqn.contains(".")) {
                    continue;
                }
                String classFqn = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
                TypeDeclaration td = context.getTypeDeclaration(classFqn);
                if (td == null) {
                    continue;
                }
                MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
                if (md != null) {
                    return md;
                }
            }
        }
        return null;
    }

    private boolean looksLikeValueOfHelper(MethodInvocation node) {
        if (node.arguments().size() != 1) {
            return false;
        }
        MethodDeclaration md = resolveHelperMethod(node);
        if (md == null) {
            return false;
        }
        if (extractUniqueValueOfArgument(md) != null) {
            return true;
        }
        if (MethodInvocationUnwrapper.getReturnedParameterIndex(md) >= 0) {
            return true;
        }
        return !extractStringCaseMap(md).isEmpty();
    }

    private String resolveExpressionTypeFqn(Expression expression, MethodDeclaration enclosing) {
        if (expression == null) {
            return null;
        }
        ITypeBinding binding = expression.resolveTypeBinding();
        if (binding != null) {
            ITypeBinding erasure = binding.getErasure();
            if (erasure != null && erasure.getQualifiedName() != null && !erasure.getQualifiedName().isBlank()) {
                return erasure.getQualifiedName();
            }
        }
        if (expression instanceof SimpleName sn && enclosing != null && variableTracer != null) {
            String methodFqn = methodFqn(enclosing);
            if (methodFqn != null) {
                String declared = variableTracer.getVariableDeclaredType(methodFqn, sn.getIdentifier());
                if (declared != null && !declared.isBlank()) {
                    int generic = declared.indexOf('<');
                    return generic >= 0 ? declared.substring(0, generic) : declared;
                }
            }
        }
        return null;
    }

    private static Expression findLocalInitializer(MethodDeclaration method, String varName) {
        return findLocalDefinition(method, varName);
    }

    /**
     * First initializer or assignment RHS for {@code varName} in the method body.
     */
    private static Expression findLocalDefinition(MethodDeclaration method, String varName) {
        if (method == null || method.getBody() == null || varName == null) {
            return null;
        }
        Expression[] found = {null};
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(VariableDeclarationStatement node) {
                for (Object fragObj : node.fragments()) {
                    if (fragObj instanceof VariableDeclarationFragment frag
                            && varName.equals(frag.getName().getIdentifier())
                            && frag.getInitializer() != null
                            && found[0] == null) {
                        found[0] = frag.getInitializer();
                    }
                }
                return super.visit(node);
            }

            @Override
            public boolean visit(Assignment node) {
                if (found[0] == null
                        && node.getLeftHandSide() instanceof SimpleName lhs
                        && varName.equals(lhs.getIdentifier())) {
                    found[0] = node.getRightHandSide();
                }
                return super.visit(node);
            }
        });
        return found[0];
    }

    private static int parameterIndex(MethodDeclaration method, String name) {
        if (method == null || name == null) {
            return -1;
        }
        List<?> params = method.parameters();
        for (int i = 0; i < params.size(); i++) {
            if (params.get(i) instanceof SingleVariableDeclaration svd
                    && name.equals(svd.getName().getIdentifier())) {
                return i;
            }
        }
        return -1;
    }

    private String methodFqn(MethodDeclaration method) {
        TypeDeclaration td = findEnclosingType(method);
        if (td == null) {
            return null;
        }
        String typeFqn = context.getFqn(td);
        if (typeFqn == null) {
            return null;
        }
        return typeFqn + "." + method.getName().getIdentifier();
    }

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

    private static Expression peel(Expression expression) {
        Expression current = expression;
        while (current instanceof ParenthesizedExpression pe) {
            current = pe.getExpression();
        }
        if (current instanceof CastExpression ce) {
            return peel(ce.getExpression());
        }
        return current;
    }

    /**
     * Peel identity-preserving String ops via {@link IdentityStringTransformSupport}
     * (0-arg trim/strip/toUpperCase/toLowerCase). Also peels Optional.of / get / orElseThrow
     * (0-arg) wrappers when the receiver is clearly an Optional/Mono/Flux factory chain —
     * not {@code Supplier.get()}. Locale/{@code replace} stay intact (fail-closed).
     */
    private static Expression peelIdentityString(Expression expression) {
        Expression current = peel(expression);
        boolean progressed;
        do {
            progressed = false;
            Expression afterStringOps = IdentityStringTransformSupport.peelIdentityStringOps(current);
            if (afterStringOps != current) {
                current = peel(afterStringOps);
                progressed = true;
                continue;
            }
            if (current instanceof MethodInvocation mi
                    && mi.arguments().isEmpty()
                    && ("get".equals(mi.getName().getIdentifier())
                    || "orElseThrow".equals(mi.getName().getIdentifier()))
                    && mi.getExpression() != null
                    && isIdentityContainerReceiver(mi.getExpression())) {
                current = peel(mi.getExpression());
                progressed = true;
                continue;
            }
            if (current instanceof MethodInvocation mi
                    && "of".equals(mi.getName().getIdentifier())
                    && mi.arguments().size() == 1
                    && mi.arguments().get(0) instanceof Expression arg) {
                String recvText = mi.getExpression() == null ? "" : mi.getExpression().toString();
                if (recvText.endsWith("Optional") || recvText.contains("Optional.")) {
                    current = peel(arg);
                    progressed = true;
                }
            }
        } while (progressed);
        return current;
    }

    private static boolean isIdentityContainerReceiver(Expression receiver) {
        if (receiver == null) {
            return false;
        }
        Expression peeled = peel(receiver);
        if (peeled instanceof MethodInvocation mi) {
            String name = mi.getName().getIdentifier();
            if ("of".equals(name) || "just".equals(name) || "empty".equals(name)) {
                return true;
            }
            if (mi.getExpression() != null && isIdentityContainerReceiver(mi.getExpression())) {
                return true;
            }
        }
        String text = peeled.toString();
        return text.contains("Optional")
                || text.endsWith("Mono")
                || text.endsWith("Flux")
                || text.contains("Mono.")
                || text.contains("Flux.");
    }

    private List<String> resolveFromMessageCarrier(
            Expression peeled,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        Expression current = peeled;
        if (current instanceof MethodInvocation mi
                && "getPayload".equals(mi.getName().getIdentifier())
                && mi.arguments().isEmpty()
                && mi.getExpression() != null) {
            MethodDeclaration enclosing = findEnclosingMethod(mi);
            Expression msg = peel(mi.getExpression());
            Expression builder = findMessageBuilderInitializer(msg, enclosing);
            if (builder != null) {
                current = builder;
            }
        }
        Expression eventExpr = MessageCarrierSupport.peelEventFromMessageExpr(current);
        if (eventExpr == null || eventExpr == peeled) {
            return List.of();
        }
        return resolveUnique(eventExpr, methodPath, callGraph, depth + 1, visited);
    }

    private List<String> resolveFromFunctionalGet(
            MethodInvocation getCall,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        String key = "fnGet:" + System.identityHashCode(getCall);
        if (!visited.add(key) || getCall.getExpression() == null) {
            return List.of();
        }
        Expression binding = findFunctionalBinding(
                peel(getCall.getExpression()), findEnclosingMethod(getCall), methodPath, callGraph);
        if (binding instanceof LambdaExpression lambda) {
            Expression body = ReactiveExpressionSupport.lambdaBodyExpression(lambda);
            if (body != null) {
                return resolveUnique(body, methodPath, callGraph, depth + 1, visited);
            }
            return List.of();
        }
        if (binding instanceof ExpressionMethodReference || binding instanceof TypeMethodReference) {
            return resolveFromMethodReference(binding, methodPath, callGraph, depth, visited);
        }
        if (binding != null && binding != getCall.getExpression()) {
            return resolveUnique(binding, methodPath, callGraph, depth + 1, visited);
        }
        return List.of();
    }

    private List<String> resolveFromMethodReference(
            Expression methodRef,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        String key = "mref:" + System.identityHashCode(methodRef);
        if (!visited.add(key)) {
            return List.of();
        }
        MethodDeclaration md = resolveMethodReferenceTarget(methodRef);
        if (md == null || md.getBody() == null) {
            return List.of();
        }
        LinkedHashSet<String> proven = new LinkedHashSet<>();
        md.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement node) {
                List<String> nested = resolveUnique(
                        node.getExpression(), methodPath, callGraph, depth + 1, visited);
                if (nested.size() == 1) {
                    proven.add(nested.get(0));
                } else if (!nested.isEmpty()) {
                    proven.add("__multi__");
                }
                return super.visit(node);
            }
        });
        proven.remove("__multi__");
        return proven.size() == 1 ? List.copyOf(proven) : List.of();
    }

    private MethodDeclaration resolveMethodReferenceTarget(Expression methodRef) {
        if (methodRef instanceof ExpressionMethodReference emr) {
            String methodName = emr.getName().getIdentifier();
            TypeDeclaration owner = typeOfExpression(emr.getExpression(), emr);
            if (owner == null) {
                owner = findEnclosingType(emr);
            }
            return owner == null ? null : context.findMethodDeclaration(owner, methodName, true);
        }
        if (methodRef instanceof TypeMethodReference tmr) {
            String methodName = tmr.getName().getIdentifier();
            // Enum.valueOf etc. need an argument — not a 0-arg Supplier.
            if ("valueOf".equals(methodName)) {
                return null;
            }
            TypeDeclaration owner = context.getTypeDeclaration(tmr.getType().toString());
            if (owner == null) {
                String simple = tmr.getType().toString();
                int generic = simple.indexOf('<');
                if (generic >= 0) {
                    simple = simple.substring(0, generic);
                }
                owner = context.getTypeDeclaration(simple);
            }
            return owner == null ? null : context.findMethodDeclaration(owner, methodName, true);
        }
        return null;
    }

    private TypeDeclaration typeOfExpression(Expression expression, ASTNode site) {
        Expression peeled = peel(expression);
        if (peeled instanceof ThisExpression) {
            return findEnclosingType(site);
        }
        if (peeled instanceof SimpleName sn) {
            String name = sn.getIdentifier();
            MethodDeclaration enclosingMethod = findEnclosingMethod(site);
            TypeDeclaration enclosingType = findEnclosingType(site);
            if (enclosingMethod != null) {
                for (Object paramObj : enclosingMethod.parameters()) {
                    if (paramObj instanceof SingleVariableDeclaration svd
                            && name.equals(svd.getName().getIdentifier())
                            && svd.getType() != null) {
                        return typeDeclarationOfAstType(svd.getType().toString());
                    }
                }
                if (enclosingMethod.getBody() != null) {
                    String[] localType = {null};
                    enclosingMethod.getBody().accept(new ASTVisitor() {
                        @Override
                        public boolean visit(VariableDeclarationStatement node) {
                            for (Object fragObj : node.fragments()) {
                                if (fragObj instanceof VariableDeclarationFragment frag
                                        && name.equals(frag.getName().getIdentifier())
                                        && node.getType() != null) {
                                    localType[0] = node.getType().toString();
                                    return false;
                                }
                            }
                            return super.visit(node);
                        }
                    });
                    if (localType[0] != null) {
                        return typeDeclarationOfAstType(localType[0]);
                    }
                }
            }
            if (enclosingType != null) {
                for (FieldDeclaration fd : enclosingType.getFields()) {
                    for (Object fragObj : fd.fragments()) {
                        if (fragObj instanceof VariableDeclarationFragment frag
                                && name.equals(frag.getName().getIdentifier())
                                && fd.getType() != null) {
                            return typeDeclarationOfAstType(fd.getType().toString());
                        }
                    }
                }
            }
            return context.getTypeDeclaration(name);
        }
        if (peeled instanceof Name name) {
            return context.getTypeDeclaration(name.getFullyQualifiedName());
        }
        return null;
    }

    private TypeDeclaration typeDeclarationOfAstType(String typeName) {
        if (typeName == null || typeName.isBlank()) {
            return null;
        }
        int generic = typeName.indexOf('<');
        if (generic >= 0) {
            typeName = typeName.substring(0, generic);
        }
        TypeDeclaration td = context.getTypeDeclaration(typeName);
        if (td == null) {
            String simple = typeName.contains(".") ? typeName.substring(typeName.lastIndexOf('.') + 1) : typeName;
            td = context.getTypeDeclaration(simple);
        }
        return td;
    }

    private Expression findFunctionalBinding(
            Expression receiver,
            MethodDeclaration enclosing,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        Expression peeled = peel(receiver);
        if (peeled instanceof LambdaExpression lambda) {
            return lambda;
        }
        if (peeled instanceof ExpressionMethodReference || peeled instanceof TypeMethodReference) {
            return peeled;
        }
        if (!(peeled instanceof SimpleName sn) || enclosing == null) {
            return null;
        }
        Expression init = findLocalInitializer(enclosing, sn.getIdentifier());
        if (init != null) {
            return peel(init);
        }
        if (variableTracer != null) {
            for (Expression def : variableTracer.traceVariableAll(peeled)) {
                Expression defPeeled = peel(def);
                if (defPeeled instanceof LambdaExpression
                        || defPeeled instanceof ExpressionMethodReference
                        || defPeeled instanceof TypeMethodReference) {
                    return defPeeled;
                }
            }
        }
        return findCallerArgumentExpression(sn, enclosing, methodPath, callGraph);
    }

    private Expression findCallerArgumentExpression(
            SimpleName paramName,
            MethodDeclaration enclosing,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph) {
        if (paramName == null || enclosing == null || methodPath == null || callGraph == null) {
            return null;
        }
        int paramIndex = parameterIndex(enclosing, paramName.getIdentifier());
        if (paramIndex < 0) {
            return null;
        }
        String calleeFqn = methodFqn(enclosing);
        if (calleeFqn == null) {
            return null;
        }
        int calleeIdx = methodPath.indexOf(calleeFqn);
        if (calleeIdx <= 0) {
            calleeIdx = indexOfMethodOnPath(methodPath, calleeFqn);
        }
        if (calleeIdx <= 0) {
            return null;
        }
        String callerMethodFqn = methodPath.get(calleeIdx - 1);
        List<CallEdge> edges = callGraph.get(callerMethodFqn);
        if (edges == null || edges.isEmpty()) {
            return null;
        }
        Expression unique = null;
        for (CallEdge edge : edges) {
            if (edge == null || edge.getTargetMethod() == null) {
                continue;
            }
            if (!CallSiteArgBinder.methodsMatch(edge.getTargetMethod(), calleeFqn)) {
                continue;
            }
            MethodDeclaration callerMd = findMethodDeclaration(callerMethodFqn);
            Expression callerArg = callerMd == null
                    ? null
                    : CallSiteArgBinder.findUniqueArgument(callerMd, edge, paramIndex).orElse(null);
            if (callerArg == null) {
                return null;
            }
            if (unique == null) {
                unique = callerArg;
            } else if (unique != callerArg
                    && !normalizeExpr(unique).equals(normalizeExpr(callerArg))) {
                return null;
            }
        }
        return unique;
    }

    private static String normalizeExpr(Expression expression) {
        return expression == null ? "" : expression.toString().replaceAll("\\s+", "");
    }

    private List<String> resolveFromHeadersGet(
            MethodInvocation headerGet,
            List<String> methodPath,
            Map<String, List<CallEdge>> callGraph,
            int depth,
            Set<String> visited) {
        String key = stringLiteralValue(headerGet.arguments().get(0));
        if (!LibraryUnwrapRegistry.isEventHeaderKey(key)) {
            return List.of();
        }
        Expression headersRecv = peel(headerGet.getExpression());
        Expression messageExpr = peelHeadersReceiver(headersRecv);
        if (messageExpr == null) {
            return List.of();
        }
        MethodDeclaration enclosing = findEnclosingMethod(headerGet);
        Expression builder = findMessageBuilderInitializer(messageExpr, enclosing);
        if (builder == null) {
            return List.of();
        }
        Expression headerEvent = MessageCarrierSupport.findSetHeaderValueOnChain(builder, key);
        if (headerEvent == null) {
            return List.of();
        }
        return resolveUnique(headerEvent, methodPath, callGraph, depth + 1, visited);
    }

    private static Expression peelHeadersReceiver(Expression headersRecv) {
        Expression peeled = peel(headersRecv);
        if (peeled instanceof MethodInvocation mi
                && "getHeaders".equals(mi.getName().getIdentifier())
                && mi.arguments().isEmpty()
                && mi.getExpression() != null) {
            return peel(mi.getExpression());
        }
        return peeled;
    }

    private Expression findMessageBuilderInitializer(Expression messageExpr, MethodDeclaration enclosing) {
        Expression peeled = peel(messageExpr);
        if (peeled instanceof MethodInvocation || peeled instanceof ClassInstanceCreation) {
            return peeled;
        }
        if (!(peeled instanceof SimpleName sn) || enclosing == null || enclosing.getBody() == null) {
            return null;
        }
        Expression init = findLocalInitializer(enclosing, sn.getIdentifier());
        return init == null ? null : peel(init);
    }

    private static String stringLiteralValue(Object arg) {
        if (arg instanceof StringLiteral sl) {
            return sl.getLiteralValue();
        }
        return null;
    }

    private static MethodInvocation findVirtualAccessorInvocation(Expression expression) {
        Expression peeled = peelIdentityString(peel(expression));
        if (peeled instanceof MethodInvocation mi
                && isVirtualAccessorName(mi.getName().getIdentifier())
                && mi.getExpression() != null
                && mi.arguments().isEmpty()) {
            return mi;
        }
        if (peeled instanceof MethodInvocation nameMi
                && "name".equals(nameMi.getName().getIdentifier())
                && nameMi.arguments().isEmpty()
                && nameMi.getExpression() != null) {
            return findVirtualAccessorInvocation(nameMi.getExpression());
        }
        if (peeled instanceof MethodInvocation valueOfMi
                && "valueOf".equals(valueOfMi.getName().getIdentifier())) {
            if (valueOfMi.arguments().size() == 1
                    && valueOfMi.arguments().get(0) instanceof Expression arg) {
                return findVirtualAccessorInvocation(arg);
            }
            if (valueOfMi.arguments().size() == 2
                    && valueOfMi.arguments().get(1) instanceof Expression arg) {
                return findVirtualAccessorInvocation(arg);
            }
        }
        return null;
    }

    private static MethodDeclaration findEnclosingMethod(ASTNode node) {
        ASTNode current = node;
        while (current != null) {
            if (current instanceof MethodDeclaration md) {
                return md;
            }
            current = current.getParent();
        }
        return null;
    }

    private static TypeDeclaration findEnclosingType(ASTNode node) {
        ASTNode current = node;
        while (current != null) {
            if (current instanceof TypeDeclaration td) {
                return td;
            }
            current = current.getParent();
        }
        return null;
    }

    private static String methodNameOf(String methodFqn) {
        return CallSiteArgBinder.methodNameOf(methodFqn);
    }

    private static int indexOfMethodOnPath(List<String> methodPath, String methodFqn) {
        if (methodPath == null || methodFqn == null) {
            return -1;
        }
        String simple = CallSiteArgBinder.methodNameOf(methodFqn);
        for (int i = methodPath.size() - 1; i >= 0; i--) {
            String hop = methodPath.get(i);
            if (hop != null && (hop.equals(methodFqn) || hop.endsWith("." + simple))) {
                return i;
            }
        }
        return -1;
    }

    private static String cacheKey(Expression expression, List<String> methodPath, int depth) {
        if (expression == null) {
            return null;
        }
        // Only cache root-ish peels (depth 0–2) to keep the map small and path-sensitive.
        if (depth > 2) {
            return null;
        }
        String pathKey = methodPath == null ? "" : String.join(">", methodPath);
        return System.identityHashCode(expression) + "@" + pathKey + "#" + depth;
    }

    private boolean expressionHasExpandHintDeep(Expression expression) {
        if (expression == null) {
            return false;
        }
        if (IdentityStringTransformSupport.expressionHasExpandHint(expression)) {
            return true;
        }
        Expression peeled = peel(expression);
        if (peeled instanceof MethodInvocation valueOfMi
                && "valueOf".equals(valueOfMi.getName().getIdentifier())
                && !valueOfMi.arguments().isEmpty()) {
            Expression arg = (Expression) valueOfMi.arguments().get(valueOfMi.arguments().size() - 1);
            if (IdentityStringTransformSupport.expressionHasExpandHint(arg)) {
                return true;
            }
            if (arg instanceof SimpleName && variableTracer != null) {
                for (Expression def : variableTracer.traceVariableAll(arg)) {
                    if (IdentityStringTransformSupport.expressionHasExpandHint(def)) {
                        return true;
                    }
                }
            }
        }
        if (peeled instanceof SimpleName && variableTracer != null) {
            for (Expression def : variableTracer.traceVariableAll(peeled)) {
                if (IdentityStringTransformSupport.expressionHasExpandHint(def)) {
                    return true;
                }
            }
        }
        return false;
    }

    private String inferExpandEnumType(Expression expression) {
        Expression peeled = peel(expression);
        if (peeled instanceof MethodInvocation valueOfMi
                && "valueOf".equals(valueOfMi.getName().getIdentifier())) {
            Expression receiver = valueOfMi.getExpression();
            if (receiver != null) {
                String fromRecv = typeNameOf(receiver);
                if (fromRecv != null) {
                    return fromRecv;
                }
            }
            if (valueOfMi.arguments().size() == 2
                    && valueOfMi.arguments().get(0) instanceof Expression classArg) {
                String fromClass = typeNameOf(classArg);
                if (fromClass != null) {
                    return fromClass;
                }
            }
            ITypeBinding binding = valueOfMi.resolveTypeBinding();
            if (binding != null && binding.isEnum()) {
                return binding.getQualifiedName();
            }
        }
        ITypeBinding binding = peeled == null ? null : peeled.resolveTypeBinding();
        if (binding != null && binding.isEnum()) {
            return binding.getQualifiedName();
        }
        return null;
    }

    private static String typeNameOf(Expression expression) {
        if (expression == null) {
            return null;
        }
        Expression current = expression;
        while (current instanceof ParenthesizedExpression pe) {
            current = pe.getExpression();
        }
        if (current instanceof SimpleName sn) {
            return sn.getIdentifier();
        }
        if (current instanceof QualifiedName qn) {
            return qn.getFullyQualifiedName();
        }
        if (current instanceof Name name) {
            return name.getFullyQualifiedName();
        }
        ITypeBinding binding = current.resolveTypeBinding();
        if (binding != null) {
            String qn = binding.getQualifiedName();
            if (qn != null && !qn.isBlank()) {
                return qn;
            }
            return binding.getName();
        }
        return current.toString();
    }

    /**
     * Path-owned / same-class helper returns with expand hints (e.g. {@code valueOf(x).name()}).
     */
    private ExpandConsult expandViaHelperPath(Expression expression, List<String> methodPath) {
        if (expression == null || methodPath == null || methodPath.isEmpty()) {
            return ExpandConsult.empty();
        }
        MethodDeclaration enclosing = findEnclosingMethod(expression);
        String enclosingClass = enclosingClassFqn(enclosing);
        List<Expression> candidates = new ArrayList<>();
        collectExpandCandidates(expression, candidates);
        for (Expression candidate : candidates) {
            if (!(candidate instanceof MethodInvocation localMethod)) {
                continue;
            }
            if (localMethod.getExpression() != null
                    && !(localMethod.getExpression() instanceof ThisExpression)) {
                continue;
            }
            String helperName = localMethod.getName().getIdentifier();
            MethodDeclaration helper = resolvePathHelper(methodPath, enclosingClass, helperName);
            if (helper == null) {
                continue;
            }
            if (!IdentityStringTransformSupport.returnExpressionsHaveExpandHint(helper)) {
                continue;
            }
            String enumType = inferEnumTypeFromMethod(helper);
            if (enumType == null || enumType.isBlank()) {
                continue;
            }
            List<String> members = expandDeclaredEnumMembers(enumType);
            if (!members.isEmpty()) {
                return ExpandConsult.enumExpand(members, enumType);
            }
        }
        return ExpandConsult.empty();
    }

    private void collectExpandCandidates(Expression expression, List<Expression> out) {
        if (expression == null) {
            return;
        }
        Expression peeled = peel(expression);
        if (peeled instanceof SimpleName sn) {
            if (variableTracer != null) {
                for (Expression def : variableTracer.traceVariableAll(peeled)) {
                    if (def != null) {
                        out.add(def);
                        Expression defPeeled = peel(def);
                        if (defPeeled != null && defPeeled != def) {
                            out.add(defPeeled);
                        }
                    }
                }
            }
            MethodDeclaration enclosing = findEnclosingMethod(peeled);
            Expression init = findLocalInitializer(enclosing, sn.getIdentifier());
            if (init != null) {
                out.add(init);
                Expression initPeeled = peel(init);
                if (initPeeled != null && initPeeled != init) {
                    out.add(initPeeled);
                }
            }
        }
        if (peeled instanceof MethodInvocation valueOfMi
                && "valueOf".equals(valueOfMi.getName().getIdentifier())
                && !valueOfMi.arguments().isEmpty()) {
            Expression arg = (Expression) valueOfMi.arguments().get(valueOfMi.arguments().size() - 1);
            collectExpandCandidates(arg, out);
        }
        if (peeled != null) {
            out.add(peeled);
        }
        out.add(expression);
    }

    private MethodDeclaration resolvePathHelper(
            List<String> methodPath,
            String enclosingClass,
            String helperName) {
        if (helperName == null || helperName.isBlank()) {
            return null;
        }
        // Prefer concrete path owners with a hinted method *body* (subclass overrides)
        // over abstract declaring types from includeSuper lookups.
        if (methodPath != null) {
            for (int i = methodPath.size() - 1; i >= 0; i--) {
                String methodFqn = methodPath.get(i);
                if (methodFqn == null || !methodFqn.contains(".")) {
                    continue;
                }
                String ownerFqn = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
                MethodDeclaration hinted = findHintedHelperOnOwner(ownerFqn, helperName);
                if (hinted != null) {
                    return hinted;
                }
            }
        }
        String pathMethod = findPathMethodByName(methodPath, helperName);
        if (pathMethod != null && pathMethod.contains(".")) {
            String ownerFqn = pathMethod.substring(0, pathMethod.lastIndexOf('.'));
            String methodName = pathMethod.substring(pathMethod.lastIndexOf('.') + 1);
            MethodDeclaration hinted = findHintedHelperOnOwner(ownerFqn, methodName);
            if (hinted != null) {
                return hinted;
            }
        }
        if (enclosingClass != null) {
            String fallbackOwner = findPrecedingConcretePathClass(methodPath, enclosingClass);
            if (fallbackOwner != null) {
                MethodDeclaration hinted = findHintedHelperOnOwner(fallbackOwner, helperName);
                if (hinted != null) {
                    return hinted;
                }
            }
            MethodDeclaration local = findHintedHelperOnOwner(enclosingClass, helperName);
            if (local != null) {
                return local;
            }
        }
        return null;
    }

    /**
     * Own-type method first (includeSuper=false), then inherited — but only if the
     * declaration has a body with an expand hint (skips abstract stubs).
     */
    private MethodDeclaration findHintedHelperOnOwner(String ownerFqn, String helperName) {
        TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
        if (ownerType == null) {
            return null;
        }
        MethodDeclaration own = context.findMethodDeclaration(ownerType, helperName, false);
        if (isHintedHelperBody(own)) {
            return own;
        }
        MethodDeclaration inherited = context.findMethodDeclaration(ownerType, helperName, true);
        if (inherited != own && isHintedHelperBody(inherited)) {
            return inherited;
        }
        return null;
    }

    private static boolean isHintedHelperBody(MethodDeclaration md) {
        return md != null
                && md.getBody() != null
                && IdentityStringTransformSupport.returnExpressionsHaveExpandHint(md);
    }

    private static String findPathMethodByName(List<String> path, String methodName) {
        if (path == null || methodName == null) {
            return null;
        }
        for (int i = path.size() - 1; i >= 0; i--) {
            String methodFqn = path.get(i);
            if (methodFqn != null && methodFqn.endsWith("." + methodName)) {
                return methodFqn;
            }
        }
        return null;
    }

    private static String findPrecedingConcretePathClass(List<String> path, String callerClass) {
        if (path == null || callerClass == null) {
            return null;
        }
        for (int i = path.size() - 1; i >= 0; i--) {
            String methodFqn = path.get(i);
            if (methodFqn == null || !methodFqn.contains(".")) {
                continue;
            }
            String classFqn = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
            if (!callerClass.equals(classFqn)) {
                return classFqn;
            }
        }
        return null;
    }

    private String enclosingClassFqn(MethodDeclaration enclosing) {
        if (enclosing == null) {
            return null;
        }
        ASTNode parent = enclosing.getParent();
        while (parent != null && !(parent instanceof TypeDeclaration)) {
            parent = parent.getParent();
        }
        if (parent instanceof TypeDeclaration td) {
            return context.getFqn(td);
        }
        return null;
    }

    private String inferEnumTypeFromMethod(MethodDeclaration methodDeclaration) {
        if (methodDeclaration == null || methodDeclaration.getBody() == null) {
            return null;
        }
        final String[] enumType = {null};
        methodDeclaration.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if (!"valueOf".equals(node.getName().getIdentifier()) || node.getExpression() == null) {
                    return super.visit(node);
                }
                String receiver = typeNameOf(node.getExpression());
                if (receiver != null && !receiver.isBlank() && Character.isUpperCase(receiver.charAt(0))) {
                    enumType[0] = receiver;
                    return false;
                }
                return super.visit(node);
            }
        });
        return enumType[0];
    }

    private List<String> qualifyMembers(List<String> members, String enumType) {
        if (members == null || members.isEmpty()) {
            return List.of();
        }
        String simpleType = simpleEnumTypeName(enumType);
        if (simpleType == null) {
            return List.copyOf(members);
        }
        List<String> qualified = new ArrayList<>();
        for (String member : members) {
            if (member == null || member.isBlank()) {
                continue;
            }
            String normalized = qualifyMember(member, simpleType);
            if (normalized != null && !qualified.contains(normalized)) {
                qualified.add(normalized);
            }
        }
        return qualified;
    }

    private static String qualifyMember(String member, String simpleType) {
        if (member == null || member.isBlank() || simpleType == null) {
            return member;
        }
        if (member.startsWith("<SYMBOLIC:") || member.startsWith("ENUM_SET:")) {
            return member;
        }
        String parsed = member.contains(".")
                ? member.substring(member.lastIndexOf('.', member.lastIndexOf('.') - 1) + 1)
                : member;
        if (parsed.contains(".")) {
            return parsed;
        }
        if (parsed.matches("[A-Z_][A-Z0-9_]*")) {
            return simpleType + "." + parsed;
        }
        return member;
    }

    private static String simpleEnumTypeName(String enumType) {
        if (enumType == null || enumType.isBlank()) {
            return null;
        }
        return enumType.contains(".")
                ? enumType.substring(enumType.lastIndexOf('.') + 1)
                : enumType;
    }

    private List<String> expandDeclaredEnumMembers(String declaredType) {
        if (declaredType == null || declaredType.isBlank() || context == null) {
            return List.of();
        }
        List<String> enumValues = context.getEnumValues(declaredType);
        if ((enumValues == null || enumValues.isEmpty()) && declaredType.contains(".")) {
            enumValues = context.getEnumValues(declaredType.substring(declaredType.lastIndexOf('.') + 1));
        }
        if (enumValues == null || enumValues.isEmpty()) {
            TypeDeclaration td = context.getTypeDeclaration(declaredType);
            if (td == null && declaredType.contains(".")) {
                td = context.getTypeDeclaration(declaredType.substring(declaredType.lastIndexOf('.') + 1));
            }
            if (td != null) {
                String fqn = context.getFqn(td);
                if (fqn != null) {
                    enumValues = context.getEnumValues(fqn);
                }
            }
        }
        if (enumValues == null || enumValues.isEmpty()) {
            return List.of();
        }
        String simpleType = simpleEnumTypeName(declaredType);
        List<String> expanded = new ArrayList<>();
        for (String enumValue : enumValues) {
            if (enumValue == null || enumValue.isBlank()) {
                continue;
            }
            String parsed = enumValue.contains(".")
                    ? enumValue.substring(enumValue.lastIndexOf('.', enumValue.lastIndexOf('.') - 1) + 1)
                    : enumValue;
            String qualified = qualifyMember(parsed, simpleType);
            if (qualified != null && !expanded.contains(qualified)) {
                expanded.add(qualified);
            }
        }
        return expanded;
    }

    public static boolean isSendLikeName(String name) {
        return name != null && SEND_LIKE_NAMES.contains(name);
    }

    public static boolean isVirtualAccessorName(String name) {
        if (name == null || name.isBlank()) {
            return false;
        }
        return isEventIdentityAccessorName(name)
                || (name.startsWith("get") && name.length() > 3)
                || COMPACT_ACCESSOR_NAMES.contains(name);
    }

    public static boolean isEventIdentityAccessorName(String name) {
        return "getType".equals(name)
                || "getEvent".equals(name)
                || "getId".equals(name)
                || "resolveEventCode".equals(name);
    }

    private static boolean looksLikeEnumConstant(String value) {
        if (value == null || value.isBlank()) {
            return false;
        }
        String trimmed = value.trim();
        if (trimmed.contains(".")) {
            trimmed = trimmed.substring(trimmed.lastIndexOf('.') + 1);
        }
        return trimmed.matches("[A-Z_][A-Z0-9_]*");
    }
}
