package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;

import java.util.*;

@Slf4j
@RequiredArgsConstructor
public class CallGraphBuilder {
    private final CodebaseContext context;

    public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
        Map<String, List<String>> callGraph = buildCallGraph();
        List<CallChain> chains = new ArrayList<>();

        for (EntryPoint ep : entryPoints) {
            String startMethod = ep.getClassName() + "." + ep.getMethodName();
            for (TriggerPoint tp : triggers) {
                String targetMethod = tp.getClassName() + "." + tp.getMethodName();
                List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
                if (path != null) {
                    chains.add(CallChain.builder()
                            .entryPoint(ep)
                            .triggerPoint(tp)
                            .methodChain(path)
                            .build());
                }
            }
        }
        return chains;
    }

    private Map<String, List<String>> buildCallGraph() {
        Map<String, List<String>> graph = new HashMap<>();
        for (CompilationUnit cu : context.getCompilationUnits()) {
            cu.accept(new ASTVisitor() {
                private String currentMethodFqn;

                @Override
                public boolean visit(MethodDeclaration node) {
                    TypeDeclaration td = findEnclosingType(node);
                    if (td != null) {
                        currentMethodFqn = context.getFqn(td) + "." + node.getName().getIdentifier();
                    }
                    return super.visit(node);
                }

                @Override
                public boolean visit(MethodInvocation node) {
                    if (currentMethodFqn != null) {
                        List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
                        for (String calledMethod : calledMethods) {
                            graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(calledMethod);
                        }
                    }
                    return super.visit(node);
                }
            });
        }
        return graph;
    }

    private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
        String baseCalled = resolveCalledMethod(node);
        if (baseCalled == null) return Collections.emptyList();

        List<String> allResolved = new ArrayList<>();
        allResolved.add(baseCalled);

        // Polymorphic fan-out
        if (baseCalled.contains(".")) {
            String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
            String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
            
            List<String> impls = context.getImplementations(className);
            for (String impl : impls) {
                allResolved.add(impl + "." + methodName);
            }
        }

        return allResolved;
    }

    private String resolveCalledMethod(MethodInvocation node) {
        Expression receiver = node.getExpression();
        String methodName = node.getName().getIdentifier();

        if (receiver == null) {
            // Local call or static import (simplified)
            TypeDeclaration td = findEnclosingType(node);
            return td != null ? context.getFqn(td) + "." + methodName : null;
        }

        // Try to resolve receiver type
        // This is a simplified resolution without full JDT bindings.
        // It works for local variables and fields within the same project.
        ITypeBinding binding = receiver.resolveTypeBinding();
        if (binding != null) {
            return binding.getQualifiedName() + "." + methodName;
        }

        // Fallback: heuristic resolution for simple cases like 'orderService.process()'
        if (receiver instanceof SimpleName sn) {
            String receiverName = sn.getIdentifier();
            // Look for fields or variables (simplified)
            // For now, we'll return a "best guess" if we can't resolve it perfectly
            // In a real implementation, we'd use JDT bindings more heavily.
            return receiverName + "." + methodName; 
        }

        return null;
    }

    private List<String> findPath(String start, String target, Map<String, List<String>> graph, Set<String> visited) {
        if (start.equals(target)) return new ArrayList<>(List.of(start));
        if (!visited.add(start)) return null;

        List<String> neighbors = graph.get(start);
        if (neighbors != null) {
            for (String neighbor : neighbors) {
                // Heuristic: neighbor might be 'orderService.process' while target is 'click.kamil...OrderService.process'
                if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
                    return new ArrayList<>(List.of(start, target));
                }
                List<String> path = findPath(neighbor, target, graph, visited);
                if (path != null) {
                    path.add(0, start);
                    return path;
                }
            }
        }
        return null;
    }

    private boolean isHeuristicMatch(String neighbor, String target) {
        // Match 'orderService.process' with 'click.kamil.OrderService.process'
        if (target.endsWith("." + neighbor)) return true;
        // Match simple names if we don't have FQN
        String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
        String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
        
        // Very basic heuristic for now
        return neighbor.toLowerCase().contains(simpleTarget.toLowerCase()) || target.toLowerCase().contains(simpleNeighbor.toLowerCase());
    }

    private TypeDeclaration findEnclosingType(ASTNode node) {
        ASTNode parent = node.getParent();
        while (parent != null && !(parent instanceof TypeDeclaration)) {
            parent = parent.getParent();
        }
        return (TypeDeclaration) parent;
    }
}
