package click.kamil.springstatemachineexporter.analysis.flow;

import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Shared binder: {@link CallEdge} argument text → unique AST argument expression in the caller body.
 * Fail-closed when zero or multiple sites match (no pick-any).
 * Used by allocation facts and event-identity string forwarding.
 */
public final class CallSiteArgBinder {

    private CallSiteArgBinder() {
    }

    /**
     * @return the unique caller AST argument for {@code edge}'s {@code paramIndex}, or empty
     */
    public static Optional<Expression> findUniqueArgument(
            MethodDeclaration callerBody,
            CallEdge edge,
            int paramIndex) {
        if (callerBody == null || callerBody.getBody() == null || edge == null || edge.getTargetMethod() == null) {
            return Optional.empty();
        }
        if (paramIndex < 0) {
            return Optional.empty();
        }
        String targetSimple = methodNameOf(edge.getTargetMethod());
        if (targetSimple == null || targetSimple.isBlank()) {
            return Optional.empty();
        }
        String edgeArgText = null;
        if (edge.getArguments() != null && paramIndex < edge.getArguments().size()) {
            edgeArgText = normalizeExprText(edge.getArguments().get(paramIndex));
        }
        List<Expression> matches = new ArrayList<>();
        callerBody.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if (!targetSimple.equals(node.getName().getIdentifier())) {
                    return super.visit(node);
                }
                if (paramIndex >= node.arguments().size()) {
                    return super.visit(node);
                }
                matches.add((Expression) node.arguments().get(paramIndex));
                return super.visit(node);
            }
        });
        if (matches.isEmpty()) {
            return Optional.empty();
        }
        if (matches.size() == 1) {
            return Optional.of(matches.get(0));
        }
        if (edgeArgText == null || edgeArgText.isBlank()) {
            return Optional.empty();
        }
        List<Expression> textHits = new ArrayList<>();
        for (Expression candidate : matches) {
            if (edgeArgText.equals(normalizeExprText(candidate.toString()))) {
                textHits.add(candidate);
            }
        }
        if (textHits.isEmpty()) {
            return Optional.empty();
        }
        // Identical arg text at several sites is equivalent — pick the first.
        // Differing texts that somehow normalize equal still share one edge string.
        return Optional.of(textHits.get(0));
    }

    /**
     * Whether two method FQNs refer to the same simple method (path hop matching).
     */
    public static boolean methodsMatch(String left, String right) {
        if (left == null || right == null) {
            return false;
        }
        if (left.equals(right)) {
            return true;
        }
        String leftSimple = methodNameOf(left);
        String rightSimple = methodNameOf(right);
        return leftSimple != null && leftSimple.equals(rightSimple)
                && (left.endsWith("." + rightSimple) || right.endsWith("." + leftSimple));
    }

    public static String methodNameOf(String methodFqn) {
        if (methodFqn == null) {
            return null;
        }
        int lastDot = methodFqn.lastIndexOf('.');
        return lastDot >= 0 ? methodFqn.substring(lastDot + 1) : methodFqn;
    }

    public static String normalizeExprText(String text) {
        if (text == null) {
            return null;
        }
        return text.replaceAll("\\s+", "");
    }

    public static String normalizeExprText(Expression expression) {
        if (expression == null) {
            return null;
        }
        return normalizeExprText(expression.toString());
    }
}
