package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import lombok.extern.slf4j.Slf4j;
import java.util.*;

@Slf4j
public class CallGraphPathFinder {
    private final CodebaseContext context;
    private final Map<java.util.Map.Entry<String, String>, Boolean> heuristicCache = new HashMap<>();

    private final Map<java.util.Map.Entry<String, String>, List<List<String>>> memoCache = new HashMap<>();
    private final Map<java.util.Map.Entry<String, String>, Boolean> unreachableCache = new HashMap<>();

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

    public List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
        if (start.equals(target)) return new ArrayList<>(List.of(start));
        if (!visited.add(start)) {
            if (log.isDebugEnabled()) {
                log.debug("Cycle detected in call graph search: {} is already in visited path {}", start, visited);
            }
            return null; // Path-scoped cycle detection
        }

        List<CallEdge> neighbors = graph.get(start);
        if (neighbors != null) {
            for (CallEdge edge : neighbors) {
                String neighbor = edge.getTargetMethod();
                if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
                    visited.remove(start);
                    return new ArrayList<>(List.of(start, target));
                }
                List<String> path = findPath(neighbor, target, graph, visited);
                if (path != null) {
                    path.add(0, start);
                    visited.remove(start);
                    return path;
                }
            }
        }
        
        if (log.isDebugEnabled()) {
            log.debug("Path search dead-end at {} when looking for {}", start, target);
        }
        
        visited.remove(start);
        return null;
    }

    public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
        return findAllPaths(start, target, graph, visited, new boolean[]{false});
    }

    public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited, boolean[] parentHitCycle) {
        List<List<String>> result = new ArrayList<>();
        java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target);
        if (unreachableCache.containsKey(cacheKey)) {
            return result;
        }
        if (memoCache.containsKey(cacheKey)) {
            List<List<String>> cached = memoCache.get(cacheKey);
            for (List<String> path : cached) {
                boolean hasCycle = false;
                for (String node : path) {
                    if (visited.contains(node)) {
                        hasCycle = true;
                        parentHitCycle[0] = true;
                        break;
                    }
                }
                if (!hasCycle) {
                    result.add(new ArrayList<>(path));
                }
            }
            return result;
        }
        if (start.equals(target)) {
            result.add(new ArrayList<>(List.of(start)));
            return result;
        }
        if (!visited.add(start)) {
            parentHitCycle[0] = true;
            return result;
        }

        boolean[] localHitCycle = new boolean[]{false};

        List<CallEdge> neighbors = graph.get(start);
        if (neighbors != null) {
            for (CallEdge edge : neighbors) {
                String neighbor = edge.getTargetMethod();
                if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) continue;
                if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
                    List<String> path = new ArrayList<>(List.of(start, target));
                    result.add(path);
                } else {
                    List<List<String>> subPaths = findAllPaths(neighbor, target, graph, visited, localHitCycle);
                    for (List<String> subPath : subPaths) {
                        List<String> path = new ArrayList<>();
                        path.add(start);
                        path.addAll(subPath);
                        result.add(path);
                    }
                }
            }
        }
        
        // Handle inheritance fallback for concrete class methods not in the graph
        if (result.isEmpty() && start.contains(".")) {
            String className = start.substring(0, start.lastIndexOf('.'));
            String methodName = start.substring(start.lastIndexOf('.') + 1);
            if (className.contains("<")) {
                className = className.substring(0, className.indexOf('<'));
            }
            TypeDeclaration td = context.getTypeDeclaration(className);
            if (td != null) {
                String superclass = context.getSuperclassFqn(td);
                if (superclass != null) {
                    String superMethod = superclass + "." + methodName;
                    if (graph.containsKey(superMethod)) {
                        List<List<String>> superPaths = findAllPaths(superMethod, target, graph, visited, localHitCycle);
                        for (List<String> superPath : superPaths) {
                            List<String> path = new ArrayList<>();
                            path.add(start);
                            path.addAll(superPath);
                            result.add(path);
                        }
                    }
                }
            }
        }

        // Handle implementation fallback for interfaces/superclasses (interface -> concrete class)
        if (result.isEmpty() && start.contains(".")) {
            String className = start.substring(0, start.lastIndexOf('.'));
            String methodName = start.substring(start.lastIndexOf('.') + 1);
            if (className.contains("<")) {
                className = className.substring(0, className.indexOf('<'));
            }
            List<String> impls = context.getImplementations(className);
            if (impls != null) {
                for (String impl : impls) {
                    String implMethod = impl + "." + methodName;
                    if (graph.containsKey(implMethod)) {
                        List<List<String>> implPaths = findAllPaths(implMethod, target, graph, visited, localHitCycle);
                        for (List<String> implPath : implPaths) {
                            List<String> path = new ArrayList<>();
                            path.add(start);
                            path.addAll(implPath);
                            result.add(path);
                        }
                    }
                }
            }
        }
        visited.remove(start);
        
        if (localHitCycle[0]) {
            parentHitCycle[0] = true;
        } else if (result.isEmpty()) {
            unreachableCache.put(cacheKey, true);
        } else {
            List<List<String>> toCache = new ArrayList<>();
            for (List<String> path : result) {
                toCache.add(new ArrayList<>(path));
            }
            memoCache.put(cacheKey, toCache);
        }
        
        return result;
    }

    public String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
        for (String node : path) {
            List<CallEdge> edges = callGraph.get(node);
            if (edges != null) {
                for (CallEdge edge : edges) {
                    String target = edge.getTargetMethod();
                    if (target != null && (target.contains(".restore") || target.contains(".read"))) {
                        if (edge.getArguments().size() >= 2) {
                            return edge.getArguments().get(1); // The contextObj / machineId
                        }
                    }
                }
            }
        }
        return null;
    }

    public boolean isHeuristicMatch(String neighbor, String target) {
        if (neighbor == null || target == null) return false;
        if (neighbor.equals(target)) return true;

        java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(neighbor, target);
        Boolean cached = heuristicCache.get(cacheKey);
        if (cached != null) return cached;

        boolean result = calculateHeuristicMatch(neighbor, target);
        heuristicCache.put(cacheKey, result);
        return result;
    }

    private boolean calculateHeuristicMatch(String neighbor, String target) {
        if (neighbor.equals(target)) return true;

        if (isFullyQualifiedMethod(neighbor) && isFullyQualifiedMethod(target)) {
            int lastDotNeighbor = neighbor.lastIndexOf('.');
            int lastDotTarget = target.lastIndexOf('.');
            if (lastDotNeighbor < 0 || lastDotTarget < 0) return false;
            
            String methodNeighbor = neighbor.substring(lastDotNeighbor + 1);
            String methodTarget = target.substring(lastDotTarget + 1);
            
            if (!methodNeighbor.equals(methodTarget)) {
                return false;
            }
            
            String classNeighbor = neighbor.substring(0, lastDotNeighbor);
            String classTarget = target.substring(0, lastDotTarget);
            
            if (classNeighbor.equals(classTarget)) return true;
            
            String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
            String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
            
            if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
            
            List<String> impls = context.getImplementations(classTarget);
            if (impls != null) {
                for (String impl : impls) {
                    if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true;
                }
            }
            List<String> implsN = context.getImplementations(classNeighbor);
            if (implsN != null) {
                for (String impl : implsN) {
                    if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true;
                }
            }
            
            return false;
        }

        String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
        String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
        if (!simpleNeighbor.equals(simpleTarget)) return false;
        
        String classNeighbor = neighbor.contains(".") ? neighbor.substring(0, neighbor.lastIndexOf('.')) : null;
        String classTarget = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : null;
        String simpleClassTarget = classTarget != null && classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
        String simpleClassNeighbor = classNeighbor != null && classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;

        if (simpleClassNeighbor != null && simpleClassTarget != null) {
            if (simpleClassNeighbor.equalsIgnoreCase(simpleClassTarget)) return true;
            // e.g. "this" vs "com.example.Machine"
            if (simpleClassNeighbor.equals("this") || simpleClassNeighbor.equals("super")) return true;
            return false;
        }
        
        return true;
    }

    private boolean isFullyQualifiedMethod(String methodFqn) {
        if (methodFqn == null || !methodFqn.contains(".")) return false;
        int lastDot = methodFqn.lastIndexOf('.');
        String classPart = methodFqn.substring(0, lastDot);
        if (classPart.contains(".")) return true;
        if (!classPart.isEmpty() && Character.isUpperCase(classPart.charAt(0))) return true;
        return false;
    }
}
