package click.kamil.springstatemachineexporter.analysis.spring;

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

import java.util.HashSet;
import java.util.Set;

public class SpringContextScanner extends ASTVisitor {
    private final SpringBeanRegistry registry;

    public SpringContextScanner(SpringBeanRegistry registry) {
        this.registry = registry;
    }

    @Override
    public boolean visit(TypeDeclaration node) {
        ITypeBinding typeBinding = node.resolveBinding();
        if (typeBinding == null) return true;

        if (isSpringComponent(typeBinding, node)) {
            SpringBean bean = SpringBean.builder()
                    .typeFqn(typeBinding.getQualifiedName())
                    .assignableTypes(getAssignableTypes(typeBinding))
                    .isPrimary(hasPrimary(typeBinding, node))
                    .qualifiers(extractQualifiers(typeBinding))
                    .order(extractOrder(typeBinding))
                    .declaringClassFqn(typeBinding.getQualifiedName())
                    .build();

            // Default bean name is uncapitalized simple name
            String defaultName = Character.toLowerCase(node.getName().getIdentifier().charAt(0)) + node.getName().getIdentifier().substring(1);
            
            // Check if @Component or @Service specifies a name (e.g., @Service("myService"))
            String explicitName = extractStereotypeValue(typeBinding);
            if (explicitName != null && !explicitName.isEmpty()) {
                bean.getBeanNames().add(explicitName);
            } else {
                bean.getBeanNames().add(defaultName);
            }

            registry.addBean(bean);
        }
        return true;
    }

    @Override
    public boolean visit(MethodDeclaration node) {
        IMethodBinding methodBinding = node.resolveBinding();
        if (methodBinding == null) return true;

        if (hasAnnotation(methodBinding, "org.springframework.context.annotation.Bean")) {
            ITypeBinding returnType = methodBinding.getReturnType();
            if (returnType != null) {
                Set<String> assignables = getAssignableTypes(returnType);
                String[] concreteType = new String[] { returnType.getQualifiedName() };
                
                if (node.getBody() != null) {
                    node.getBody().accept(new ASTVisitor() {
                        @Override
                        public boolean visit(ReturnStatement ret) {
                            if (ret.getExpression() != null) {
                                ITypeBinding exprType = ret.getExpression().resolveTypeBinding();
                                if (exprType != null && exprType.isClass()) {
                                    concreteType[0] = exprType.getQualifiedName();
                                    assignables.addAll(getAssignableTypes(exprType));
                                }
                            }
                            return super.visit(ret);
                        }
                    });
                }

                SpringBean bean = SpringBean.builder()
                        .typeFqn(concreteType[0])
                        .assignableTypes(assignables)
                        .isPrimary(hasPrimaryMethod(methodBinding, node))
                        .qualifiers(extractQualifiers(methodBinding))
                        .order(extractOrder(methodBinding))
                        .declaringClassFqn(methodBinding.getDeclaringClass() != null ? methodBinding.getDeclaringClass().getQualifiedName() : null)
                        .factoryMethodName(node.getName().getIdentifier())
                        .build();

                // Default bean name is method name
                String defaultName = node.getName().getIdentifier();
                
                // Check if @Bean specifies a name (e.g., @Bean("myBean"))
                String explicitName = extractBeanName(methodBinding);
                if (explicitName != null && !explicitName.isEmpty()) {
                    bean.getBeanNames().add(explicitName);
                } else {
                    bean.getBeanNames().add(defaultName);
                }

                registry.addBean(bean);
            }
        }
        return true;
    }

    private boolean isSpringComponent(ITypeBinding binding, TypeDeclaration node) {
        if (hasMetaAnnotation(binding, "org.springframework.stereotype.Component") ||
               hasMetaAnnotation(binding, "org.springframework.web.bind.annotation.RestController") ||
               hasAnnotation(binding, "org.springframework.stereotype.Component") ||
               hasAnnotation(binding, "org.springframework.stereotype.Service") ||
               hasAnnotation(binding, "org.springframework.stereotype.Repository") ||
               hasAnnotation(binding, "org.springframework.stereotype.Controller") ||
               hasAnnotation(binding, "org.springframework.web.bind.annotation.RestController") ||
               hasAnnotation(binding, "org.springframework.context.annotation.Configuration")) {
            return true;
        }
        for (Object modObj : node.modifiers()) {
            if (modObj instanceof Annotation ann) {
                String name = ann.getTypeName().getFullyQualifiedName();
                if (name.equals("Component") || name.equals("Service") || name.equals("Repository") ||
                    name.equals("Controller") || name.equals("RestController") || name.equals("Configuration")) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean hasPrimary(ITypeBinding binding, TypeDeclaration node) {
        if (hasAnnotation(binding, "org.springframework.context.annotation.Primary")) return true;
        for (Object modObj : node.modifiers()) {
            if (modObj instanceof Annotation ann) {
                String name = ann.getTypeName().getFullyQualifiedName();
                if (name.equals("Primary")) return true;
            }
        }
        return false;
    }

    private boolean hasPrimaryMethod(IMethodBinding binding, MethodDeclaration node) {
        if (hasAnnotation(binding, "org.springframework.context.annotation.Primary")) return true;
        for (Object modObj : node.modifiers()) {
            if (modObj instanceof Annotation ann) {
                String name = ann.getTypeName().getFullyQualifiedName();
                if (name.equals("Primary")) return true;
            }
        }
        return false;
    }
    private boolean hasMetaAnnotation(ITypeBinding binding, String targetFqn) {
        return checkMetaAnnotation(binding.getAnnotations(), targetFqn, new HashSet<>());
    }
    
    private boolean checkMetaAnnotation(IAnnotationBinding[] annotations, String targetFqn, Set<String> visited) {
        for (IAnnotationBinding ann : annotations) {
            ITypeBinding annType = ann.getAnnotationType();
            if (annType != null) {
                String fqn = annType.getQualifiedName();
                if (fqn.equals(targetFqn)) return true;
                
                // Avoid circular meta-annotations (e.g. Documented -> Documented)
                if (visited.add(fqn)) {
                    if (checkMetaAnnotation(annType.getAnnotations(), targetFqn, visited)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean hasAnnotation(IBinding binding, String annotationFqn) {
        IAnnotationBinding[] annotations = binding.getAnnotations();
        for (IAnnotationBinding ann : annotations) {
            if (ann.getAnnotationType() != null && annotationFqn.equals(ann.getAnnotationType().getQualifiedName())) {
                return true;
            }
        }
        return false;
    }

    private Set<String> getAssignableTypes(ITypeBinding binding) {
        Set<String> types = new HashSet<>();
        collectAssignableTypes(binding, types);
        return types;
    }

    private void collectAssignableTypes(ITypeBinding binding, Set<String> types) {
        if (binding == null) return;
        
        String qName = binding.getQualifiedName();
        if (qName != null && !qName.isEmpty()) {
            types.add(qName);
        }
        
        ITypeBinding erasure = binding.getErasure();
        if (erasure != null) {
            String erasureName = erasure.getQualifiedName();
            if (erasureName != null && !erasureName.isEmpty()) {
                types.add(erasureName);
            }
        }

        collectAssignableTypes(binding.getSuperclass(), types);
        if (binding.getInterfaces() != null) {
            for (ITypeBinding iface : binding.getInterfaces()) {
                collectAssignableTypes(iface, types);
            }
        }
    }

    private Set<String> extractQualifiers(IBinding binding) {
        Set<String> qualifiers = new HashSet<>();
        for (IAnnotationBinding ann : binding.getAnnotations()) {
            if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
                for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
                    if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
                        qualifiers.add((String) pair.getValue());
                    }
                }
            }
        }
        return qualifiers;
    }

    private Integer extractOrder(IBinding binding) {
        for (IAnnotationBinding ann : binding.getAnnotations()) {
            if (ann.getAnnotationType() != null && "org.springframework.core.annotation.Order".equals(ann.getAnnotationType().getQualifiedName())) {
                for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
                    if ("value".equals(pair.getName())) {
                        Object val = pair.getValue();
                        if (val instanceof Integer) {
                            return (Integer) val;
                        } else if (val instanceof IVariableBinding) {
                            Object constantValue = ((IVariableBinding) val).getConstantValue();
                            if (constantValue instanceof Integer) {
                                return (Integer) constantValue;
                            }
                        }
                    }
                }
                return Integer.MAX_VALUE; // Spring's default for @Order()
            }
        }
        return null;
    }

    private String extractStereotypeValue(ITypeBinding binding) {
        for (IAnnotationBinding ann : binding.getAnnotations()) {
            if (ann.getAnnotationType() == null) continue;
            String fqn = ann.getAnnotationType().getQualifiedName();
            if (checkMetaAnnotation(new IAnnotationBinding[]{ann}, "org.springframework.stereotype.Component", new HashSet<>()) ||
                "org.springframework.stereotype.Component".equals(fqn) ||
                "org.springframework.stereotype.Service".equals(fqn) ||
                "org.springframework.stereotype.Repository".equals(fqn) ||
                "org.springframework.stereotype.Controller".equals(fqn) ||
                "org.springframework.web.bind.annotation.RestController".equals(fqn) ||
                "org.springframework.context.annotation.Configuration".equals(fqn)) {
                for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
                    if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
                        return (String) pair.getValue();
                    }
                }
            }
        }
        return null;
    }

    private String extractBeanName(IMethodBinding binding) {
        for (IAnnotationBinding ann : binding.getAnnotations()) {
            if ("org.springframework.context.annotation.Bean".equals(ann.getAnnotationType().getQualifiedName())) {
                for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
                    if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof String) {
                        return (String) pair.getValue();
                    } else if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof Object[]) {
                        Object[] vals = (Object[]) pair.getValue();
                        if (vals.length > 0 && vals[0] instanceof String) {
                            return (String) vals[0];
                        }
                    }
                }
            }
        }
        return null;
    }
}
