package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;

import org.eclipse.jdt.core.dom.WildcardType;

public class TypeResolver {
    private final CodebaseContext context;

    private static final Set<String> GENERIC_EVENT_PARAMETER_NAMES = Set.of("e", "ev");

    public TypeResolver(CodebaseContext context) {
        this.context = context;
    }

    public String resolveTypeToFqn(Type type, ASTNode contextNode) {
        CompilationUnit cu = contextNode != null && contextNode.getRoot() instanceof CompilationUnit rootCu
                ? rootCu
                : null;
        return resolveTypeToFqn(type, cu, Map.of());
    }

    public String resolveTypeToFqn(Type type, CompilationUnit cu) {
        return resolveTypeToFqn(type, cu, Map.of());
    }

    public String resolveTypeToFqn(Type type, CompilationUnit cu, Map<String, String> typeVarBindings) {
        if (type == null) {
            return null;
        }
        if (type instanceof WildcardType wildcardType) {
            Type bound = wildcardType.getBound();
            return bound != null ? resolveTypeToFqn(bound, cu, typeVarBindings) : null;
        }
        String simpleName;
        if (type.isSimpleType()) {
            simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
        } else if (type.isParameterizedType()) {
            return resolveTypeToFqn(((ParameterizedType) type).getType(), cu, typeVarBindings);
        } else {
            simpleName = type.toString();
        }

        if (simpleName.contains("<")) {
            simpleName = simpleName.substring(0, simpleName.indexOf('<'));
        }

        if (typeVarBindings.containsKey(simpleName)) {
            return typeVarBindings.get(simpleName);
        }

        if (cu != null) {
            AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
            if (td != null) {
                return context.getFqn(td);
            }

            for (Object impObj : cu.imports()) {
                ImportDeclaration imp = (ImportDeclaration) impObj;
                String impName = imp.getName().getFullyQualifiedName();
                if (impName.endsWith("." + simpleName)) {
                    return impName;
                }
            }

            String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
            if (!packageName.isEmpty()) {
                AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
                if (localTd != null) {
                    return context.getFqn(localTd);
                }
            }
        } else {
            AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName);
            if (td != null) {
                return context.getFqn(td);
            }
        }

        return preferBindingFqn(type, simpleName);
    }

    private String preferBindingFqn(Type type, String heuristic) {
        if (!context.isResolveBindings() || heuristic == null || heuristic.contains(".")) {
            return heuristic;
        }
        ITypeBinding binding = type.resolveBinding();
        if (binding == null || binding.isRecovered()) {
            return heuristic;
        }
        String bindingFqn = binding.getErasure().getQualifiedName();
        if (bindingFqn == null || bindingFqn.isBlank() || !bindingFqn.contains(".") || bindingFqn.startsWith("<")) {
            return heuristic;
        }
        if (bindingFqn.startsWith("java.") || bindingFqn.startsWith("javax.") || bindingFqn.startsWith("jakarta.")) {
            return heuristic;
        }
        return bindingFqn;
    }

    public int getParameterIndex(String methodFqn, String paramName) {
        return getParameterIndex(methodFqn, paramName, false);
    }

    /**
     * When {@code allowSyntheticEventPlaceholder} is true, maps generic trigger placeholders
     * such as {@code e} onto the sole parameter of single-arg methods (e.g. {@code sendEvent}).
     */
    public int getParameterIndex(String methodFqn, String paramName, boolean allowSyntheticEventPlaceholder) {
        if (methodFqn == null || !methodFqn.contains(".")) return -1;
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td != null) {
            MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
            if (md != null) {
                for (int i = 0; i < md.parameters().size(); i++) {
                    SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
                    if (svd.getName().getIdentifier().equals(paramName)) {
                        return i;
                    }
                }
                if (allowSyntheticEventPlaceholder
                        && md.parameters().size() == 1
                        && isGenericEventParameterName(paramName)) {
                    return 0;
                }
            }
        }
        return -1;
    }

    private static boolean isGenericEventParameterName(String paramName) {
        return paramName != null && GENERIC_EVENT_PARAMETER_NAMES.contains(paramName);
    }

    public List<String> getParameterNames(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;
        }
        MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
        if (md == null) {
            return null;
        }
        List<String> paramNames = new ArrayList<>();
        for (Object paramObj : md.parameters()) {
            SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
            paramNames.add(param.getName().getIdentifier());
        }
        return paramNames;
    }

    public String getParameterName(String methodFqn, int index) {
        if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td != null) {
            MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
            if (md != null && index < md.parameters().size()) {
                SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index);
                return svd.getName().getIdentifier();
            }
        }
        return null;
    }

    public String getParameterType(String methodFqn, int index) {
        if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td != null) {
            MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
            if (md != null && index < md.parameters().size()) {
                SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index);
                return svd.getType().toString();
            }
        }
        return null;
    }

    /**
     * Resolves parameter type from the actual call site in {@code callerFqn}, so overloaded
     * targets (e.g. {@code send(OrderEvent)} vs {@code send(DocumentEvent)}) use the invoked overload.
     */
    public String getParameterTypeAtCallSite(String callerFqn, String calleeFqn, int paramIndex) {
        if (callerFqn == null || calleeFqn == null || paramIndex < 0
                || !callerFqn.contains(".") || !calleeFqn.contains(".")) {
            return null;
        }
        String callerClass = callerFqn.substring(0, callerFqn.lastIndexOf('.'));
        String callerMethod = callerFqn.substring(callerFqn.lastIndexOf('.') + 1);
        String calleeMethod = calleeFqn.substring(calleeFqn.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(callerClass);
        if (td == null) {
            return null;
        }
        MethodDeclaration md = context.findMethodDeclaration(td, callerMethod, true);
        if (md == null || md.getBody() == null) {
            return null;
        }
        final String[] result = new String[1];
        md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
            @Override
            public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) {
                if (!calleeMethod.equals(node.getName().getIdentifier())) {
                    return super.visit(node);
                }
                org.eclipse.jdt.core.dom.IMethodBinding binding = node.resolveMethodBinding();
                if (binding != null && binding.getParameterTypes().length > paramIndex) {
                    result[0] = binding.getParameterTypes()[paramIndex].getErasure().getQualifiedName();
                }
                return super.visit(node);
            }
        });
        return result[0];
    }

    public boolean isTypeCompatible(String actualType, String expectedType) {
        if (actualType == null || expectedType == null) return true;
        if (expectedType.equals("Object") || expectedType.equals("java.lang.Object")) return true;
        if (actualType.equals("Object") || actualType.equals("java.lang.Object")) return true;
        if (expectedType.length() == 1 && Character.isUpperCase(expectedType.charAt(0))) return true;
        if (actualType.length() == 1 && Character.isUpperCase(actualType.charAt(0))) return true;
        
        String originalExpectedType = expectedType;

        if (actualType.contains("<")) {
            String rawType = actualType.substring(0, actualType.indexOf('<'));
            if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
                int start = actualType.indexOf('<');
                int end = actualType.lastIndexOf('>');
                if (start >= 0 && end > start) {
                    actualType = actualType.substring(start + 1, end);
                }
            }
        }
        if (expectedType.contains("<")) {
            String rawType = expectedType.substring(0, expectedType.indexOf('<'));
            if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
                int start = expectedType.indexOf('<');
                int end = expectedType.lastIndexOf('>');
                if (start >= 0 && end > start) {
                    expectedType = expectedType.substring(start + 1, end);
                }
            }
        }

        if (actualType.contains("<")) {
            actualType = actualType.substring(0, actualType.indexOf('<'));
        }
        if (expectedType.contains("<")) {
            expectedType = expectedType.substring(0, expectedType.indexOf('<'));
        }
        
        if (actualType.equals(expectedType)) return true;

        String expectedSimple = expectedType.contains(".")
                ? expectedType.substring(expectedType.lastIndexOf('.') + 1)
                : expectedType;
        String actualSimple = actualType.contains(".")
                ? actualType.substring(actualType.lastIndexOf('.') + 1)
                : actualType;
        if (expectedSimple.equals(actualSimple) && context.isAmbiguousSimpleName(expectedSimple)) {
            return false;
        }

        if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true;

        AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType);
        if (td != null) {
            if (td instanceof TypeDeclaration typeDecl) {
                String superFqn = context.getSuperclassFqn(typeDecl);
                TypeDeclaration superTd = superFqn != null ? context.getTypeDeclaration(superFqn) : null;
                if (superTd != null) {
                    final String normalizedExpectedType = expectedType;
                    final boolean[] matched = {false};
                    context.forEachSuperclass(superTd, current -> {
                        if (matched[0]) {
                            return;
                        }
                        String currentFqn = context.getFqn(current);
                        if (currentFqn == null) {
                            return;
                        }
                        if (currentFqn.contains("<")) {
                            currentFqn = currentFqn.substring(0, currentFqn.indexOf('<'));
                        }
                        if (currentFqn.equals(normalizedExpectedType)
                                || currentFqn.endsWith("." + normalizedExpectedType)
                                || normalizedExpectedType.endsWith("." + currentFqn)) {
                            matched[0] = true;
                        }
                    });
                    if (matched[0]) {
                        return true;
                    }
                }
            }
            List interfaces = java.util.Collections.emptyList();
            if (td instanceof TypeDeclaration typeDecl) {
                interfaces = typeDecl.superInterfaceTypes();
            } else if (td instanceof EnumDeclaration enumDecl) {
                interfaces = enumDecl.superInterfaceTypes();
            } else if (td instanceof RecordDeclaration recordDecl) {
                interfaces = recordDecl.superInterfaceTypes();
            }
            for (Object interfaceType : interfaces) {
                String itStr = interfaceType.toString();
                if (itStr.contains("<")) {
                    itStr = itStr.substring(0, itStr.indexOf('<'));
                }
                if (itStr.equals(expectedType) || itStr.endsWith("." + expectedType) || expectedType.endsWith("." + itStr)) return true;
            }
        }

        if (originalExpectedType.contains("<")) {
            int start = originalExpectedType.indexOf('<');
            int end = originalExpectedType.lastIndexOf('>');
            if (start >= 0 && end > start) {
                String sub = originalExpectedType.substring(start + 1, end);
                for (String part : sub.split(",")) {
                    String typeArg = part.trim();
                    if (isTypeCompatible(actualType, typeArg)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
