package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * Matches {@link CallEdge}s to concrete {@link MethodInvocation}s in caller source when
 * multiple call sites target the same method.
 */
public final class CallSiteMatcher {

    private static final Pattern ENUM_LITERAL = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*\\.[A-Z_][A-Z0-9_]*");

    private CallSiteMatcher() {
    }

    public static CallEdge selectEdge(
            String caller,
            String target,
            List<CallEdge> edges,
            Map<String, String> bindings) {
        if (edges == null || edges.isEmpty()) {
            return null;
        }
        List<CallEdge> matching = new ArrayList<>();
        for (CallEdge edge : edges) {
            if (edge.getTargetMethod().equals(target) || heuristicTargetMatch(edge.getTargetMethod(), target)) {
                matching.add(edge);
            }
        }
        if (matching.isEmpty()) {
            return null;
        }
        if (matching.size() == 1) {
            return matching.get(0);
        }

        List<CallEdge> constraintFiltered = matching;
        if (bindings != null && !bindings.isEmpty()) {
            List<CallEdge> compatible = new ArrayList<>();
            for (CallEdge edge : matching) {
                String constraint = edge.getConstraint();
                if (constraint == null
                        || constraint.isBlank()
                        || BooleanConstraintEvaluator.isCompatibleWithBindings(constraint, bindings)) {
                    compatible.add(edge);
                }
            }
            if (!compatible.isEmpty()) {
                constraintFiltered = compatible;
            }
        }
        if (constraintFiltered.size() == 1) {
            return constraintFiltered.get(0);
        }

        if (bindings != null && !bindings.isEmpty()) {
            for (CallEdge edge : constraintFiltered) {
                if (edge.getArguments() == null) {
                    continue;
                }
                for (String edgeArg : edge.getArguments()) {
                    for (String bindingValue : bindings.values()) {
                        if (edgeArg == null || bindingValue == null) {
                            continue;
                        }
                        if (edgeArg.contains(bindingValue) || bindingValue.contains(edgeArg)) {
                            return edge;
                        }
                    }
                }
            }
        }

        for (CallEdge edge : constraintFiltered) {
            if (findMatchingInvocation(caller, target, edge, null) != null) {
                return edge;
            }
        }
        return constraintFiltered.get(0);
    }

    public static MethodInvocation findMatchingInvocation(
            String caller,
            String callee,
            CallEdge edge,
            CodebaseContext context) {
        List<MethodInvocation> invocations = findCallInvocations(caller, callee, context);
        if (invocations.isEmpty()) {
            return null;
        }
        if (edge == null || invocations.size() == 1) {
            return invocations.get(0);
        }
        for (MethodInvocation invocation : invocations) {
            if (invocationMatchesEdge(invocation, edge)) {
                return invocation;
            }
        }
        return null;
    }

    public static Expression findCallSiteArgumentExpression(
            String caller,
            String callee,
            int paramIndex,
            CallEdge edge,
            CodebaseContext context) {
        MethodInvocation invocation = findMatchingInvocation(caller, callee, edge, context);
        if (invocation == null) {
            return null;
        }
        if (paramIndex < 0 || paramIndex >= invocation.arguments().size()) {
            return null;
        }
        return (Expression) invocation.arguments().get(paramIndex);
    }

    static List<MethodInvocation> findCallInvocations(String caller, String callee, CodebaseContext context) {
        if (caller == null || callee == null || !caller.contains(".") || !callee.contains(".") || context == null) {
            return List.of();
        }
        String callerClass = caller.substring(0, caller.lastIndexOf('.'));
        String callerMethod = caller.substring(caller.lastIndexOf('.') + 1);
        String calleeMethod = callee.substring(callee.lastIndexOf('.') + 1);
        String calleeClass = callee.substring(0, callee.lastIndexOf('.'));
        TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
        if (callerType == null) {
            return List.of();
        }
        MethodDeclaration method = context.findMethodDeclaration(callerType, callerMethod, true);
        if (method == null || method.getBody() == null) {
            return List.of();
        }

        List<MethodInvocation> invocations = new ArrayList<>();
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if (!calleeMethod.equals(node.getName().getIdentifier())) {
                    return super.visit(node);
                }
                IMethodBinding binding = node.resolveMethodBinding();
                if (binding != null && binding.getDeclaringClass() != null) {
                    String declaringFqn = binding.getDeclaringClass().getErasure().getQualifiedName();
                    if (!declaringFqn.equals(calleeClass) && !declaringFqn.endsWith("." + calleeClass)) {
                        String calleeSimple = calleeClass.substring(calleeClass.lastIndexOf('.') + 1);
                        if (!declaringFqn.endsWith("." + calleeSimple)) {
                            return super.visit(node);
                        }
                    }
                }
                invocations.add(node);
                return super.visit(node);
            }
        });
        return invocations;
    }

    static boolean invocationMatchesEdge(MethodInvocation invocation, CallEdge edge) {
        if (edge == null || edge.getArguments() == null) {
            return true;
        }
        List<?> astArgs = invocation.arguments();
        List<String> edgeArgs = edge.getArguments();
        int count = Math.min(astArgs.size(), edgeArgs.size());
        if (count == 0) {
            return edgeArgs.isEmpty();
        }
        int matched = 0;
        for (int i = 0; i < count; i++) {
            if (argumentTokensMatch(((Expression) astArgs.get(i)).toString(), edgeArgs.get(i))) {
                matched++;
            }
        }
        return matched == count;
    }

    static boolean argumentTokensMatch(String astArg, String edgeArg) {
        if (astArg == null || edgeArg == null) {
            return false;
        }
        String left = normalizeArgument(astArg);
        String right = normalizeArgument(edgeArg);
        if (left.equals(right)) {
            return true;
        }
        String leftEnum = extractEnumLiteral(left);
        String rightEnum = extractEnumLiteral(right);
        return leftEnum != null && leftEnum.equals(rightEnum);
    }

    private static String normalizeArgument(String value) {
        return value.replaceAll("\\s+", "");
    }

    private static String extractEnumLiteral(String value) {
        var matcher = ENUM_LITERAL.matcher(value);
        return matcher.find() ? matcher.group() : null;
    }

    private static boolean heuristicTargetMatch(String edgeTarget, String pathTarget) {
        if (edgeTarget == null || pathTarget == null) {
            return false;
        }
        if (edgeTarget.equals(pathTarget)) {
            return true;
        }
        String edgeSimple = edgeTarget.substring(edgeTarget.lastIndexOf('.') + 1);
        String pathSimple = pathTarget.substring(pathTarget.lastIndexOf('.') + 1);
        return edgeSimple.equals(pathSimple);
    }
}
