package click.kamil.springstatemachineexporter.analysis.service;

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

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

    /** Memo key: start|target|concreteSelf */
    private final Map<String, List<List<String>>> memoCache = new HashMap<>();
    private final Map<String, Boolean> unreachableCache = new HashMap<>();

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

    public void clearAnalysisCaches() {
        heuristicCache.clear();
        memoCache.clear();
        unreachableCache.clear();
    }

    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}, null);
    }

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

    public List<List<String>> findAllPaths(
            String start,
            String target,
            Map<String, List<CallEdge>> graph,
            Set<String> visited,
            PathBindingEvaluator bindingEvaluator,
            Map<String, String> initialBindings) {
        return findAllPaths(start, target, graph, visited, bindingEvaluator, initialBindings, null);
    }

    public List<List<String>> findAllPaths(
            String start,
            String target,
            Map<String, List<CallEdge>> graph,
            Set<String> visited,
            PathBindingEvaluator bindingEvaluator,
            Map<String, String> initialBindings,
            String concreteSelfFqn) {
        if (bindingEvaluator == null) {
            return findAllPaths(start, target, graph, visited, concreteSelfFqn);
        }
        Map<String, String> bindings = initialBindings == null ? new HashMap<>() : new HashMap<>(initialBindings);
        return findAllPathsConstrained(
                start, target, graph, visited, new boolean[]{false}, bindingEvaluator, bindings, concreteSelfFqn);
    }

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

    public List<List<String>> findAllPaths(
            String start,
            String target,
            Map<String, List<CallEdge>> graph,
            Set<String> visited,
            boolean[] parentHitCycle,
            String concreteSelfFqn) {
        List<List<String>> result = new ArrayList<>();
        String cacheKey = memoKey(start, target, concreteSelfFqn);
        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};
        String selfAtStart = concreteSelfFqn;

        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;
                }
                // Prune against the current dispatch self first; only then adopt a stamped
                // collaborator receiver type for deeper hops.
                if (!acceptVirtualHop(start, neighbor, selfAtStart)) {
                    continue;
                }
                String nextSelf = nextSelfAfterEdge(selfAtStart, start, edge);
                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, nextSelf);
                    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 = classNameOf(start);
            String methodName = methodNameOf(start);
            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, selfAtStart);
                        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 = classNameOf(start);
            String methodName = methodNameOf(start);
            List<String> impls = context.getImplementations(className);
            if (impls != null) {
                for (String impl : impls) {
                    if (!acceptImplementationFallback(className, impl, selfAtStart)) {
                        continue;
                    }
                    String implMethod = impl + "." + methodName;
                    if (graph.containsKey(implMethod)) {
                        String nextSelf = nextConcreteSelfForImpl(selfAtStart, className, impl);
                        List<List<String>> implPaths =
                                findAllPaths(implMethod, target, graph, visited, localHitCycle, nextSelf);
                        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;
    }

    private List<List<String>> findAllPathsConstrained(
            String start,
            String target,
            Map<String, List<CallEdge>> graph,
            Set<String> visited,
            boolean[] parentHitCycle,
            PathBindingEvaluator bindingEvaluator,
            Map<String, String> bindings,
            String concreteSelfFqn) {
        List<List<String>> result = new ArrayList<>();
        if (start.equals(target)) {
            result.add(new ArrayList<>(List.of(start)));
            return result;
        }
        if (!visited.add(start)) {
            parentHitCycle[0] = true;
            return result;
        }

        String selfAtStart = concreteSelfFqn;
        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 (!acceptVirtualHop(start, neighbor, selfAtStart)) {
                    continue;
                }
                String nextSelf = nextSelfAfterEdge(selfAtStart, start, edge);

                String resolvedTarget = neighbor.equals(target) || isHeuristicMatch(neighbor, target) ? target : neighbor;
                List<String> hopPath = new ArrayList<>(List.of(start, resolvedTarget));
                Map<String, String> branchBindings = new HashMap<>(bindings);
                if (!bindingEvaluator.tryTraverseEdge(
                        start, resolvedTarget, edge, hopPath, 1, graph, this, branchBindings)) {
                    continue;
                }

                if (resolvedTarget.equals(target)) {
                    result.add(hopPath);
                } else {
                    List<List<String>> subPaths = findAllPathsConstrained(
                            neighbor, target, graph, visited, parentHitCycle, bindingEvaluator, branchBindings,
                            nextSelf);
                    for (List<String> subPath : subPaths) {
                        if (subPath.isEmpty() || subPath.get(0).equals(start)) {
                            continue;
                        }
                        List<String> path = new ArrayList<>();
                        path.add(start);
                        path.addAll(subPath);
                        result.add(path);
                    }
                }
            }
        }

        if (result.isEmpty() && start.contains(".")) {
            result.addAll(findInheritanceFallbackPaths(
                    start, target, graph, visited, parentHitCycle, bindingEvaluator, bindings, selfAtStart));
        }

        visited.remove(start);
        return result;
    }

    private List<List<String>> findInheritanceFallbackPaths(
            String start,
            String target,
            Map<String, List<CallEdge>> graph,
            Set<String> visited,
            boolean[] parentHitCycle,
            PathBindingEvaluator bindingEvaluator,
            Map<String, String> bindings,
            String concreteSelfFqn) {
        List<List<String>> result = new ArrayList<>();
        String className = classNameOf(start);
        String methodName = methodNameOf(start);

        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 = findAllPathsConstrained(
                            superMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings,
                            concreteSelfFqn);
                    for (List<String> superPath : superPaths) {
                        List<String> path = new ArrayList<>();
                        path.add(start);
                        path.addAll(superPath);
                        result.add(path);
                    }
                }
            }
        }

        if (result.isEmpty()) {
            List<String> impls = context.getImplementations(className);
            if (impls != null) {
                for (String impl : impls) {
                    if (!acceptImplementationFallback(className, impl, concreteSelfFqn)) {
                        continue;
                    }
                    String implMethod = impl + "." + methodName;
                    if (graph.containsKey(implMethod)) {
                        String nextSelf = nextConcreteSelfForImpl(concreteSelfFqn, className, impl);
                        List<List<String>> implPaths = findAllPathsConstrained(
                                implMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings,
                                nextSelf);
                        for (List<String> implPath : implPaths) {
                            List<String> path = new ArrayList<>();
                            path.add(start);
                            path.addAll(implPath);
                            result.add(path);
                        }
                    }
                }
            }
        }
        return result;
    }

    /**
     * Prefer a concrete {@link CallEdge#getReceiverTypeFqn()} stamped at graph construction
     * (CHA-refined collaborator impl). Otherwise fall back to self-hierarchy narrowing.
     */
    private String nextSelfAfterEdge(String concreteSelfFqn, String callerMethod, CallEdge edge) {
        if (edge != null) {
            String stamped = edge.getReceiverTypeFqn();
            if (stamped != null && context.isConcreteClassType(stamped)) {
                return stamped;
            }
        }
        String targetMethod = edge != null ? edge.getTargetMethod() : null;
        return nextConcreteSelf(concreteSelfFqn, callerMethod, targetMethod);
    }

    /**
     * Reject CHA fan-out edges that dispatch to a sibling subclass incompatible with the entry's
     * concrete runtime type — but only for self-hierarchy template dispatch
     * ({@code concreteSelf ≤ callerClass}). Collaborator abstract types
     * ({@code Controller → AbstractService → Impl}) must stay CHA-open unless a concrete
     * {@code receiverTypeFqn} was adopted as the dispatch self on the incoming edge.
     */
    private boolean acceptVirtualHop(String callerMethod, String targetMethod, String concreteSelfFqn) {
        boolean accepted;
        if (!isSelfHierarchyDispatch(callerMethod, concreteSelfFqn)) {
            accepted = true;
        } else if (!isVirtualDispatchCandidate(callerMethod, targetMethod)) {
            accepted = true;
        } else {
            String targetClass = classNameOf(targetMethod);
            accepted = targetClass != null && context.isSubtypeOrSame(concreteSelfFqn, targetClass);
        }
        if (targetMethod != null
                && targetMethod.contains("doProcessMessage")
                && log.isDebugEnabled()) {
            log.debug(
                    "acceptVirtualHop callerMethod={} targetMethod={} concreteSelfFqn={} accepted={}",
                    callerMethod,
                    targetMethod,
                    concreteSelfFqn,
                    accepted);
        }
        return accepted;
    }

    private boolean acceptImplementationFallback(
            String abstractClassFqn, String implClassFqn, String concreteSelfFqn) {
        if (!isPruningActive(concreteSelfFqn)) {
            return true;
        }
        // Only prune when the abstract/interface frame is part of the entry's own hierarchy.
        if (abstractClassFqn == null || !context.isSubtypeOrSame(concreteSelfFqn, abstractClassFqn)) {
            return true;
        }
        return context.isSubtypeOrSame(concreteSelfFqn, implClassFqn);
    }

    private boolean isPruningActive(String concreteSelfFqn) {
        return concreteSelfFqn != null
                && !concreteSelfFqn.isBlank()
                && context.isConcreteClassType(concreteSelfFqn);
    }

    /**
     * True when {@code concreteSelf} is a concrete subtype of the caller frame — i.e. we are
     * walking inherited/template-method code belonging to the entry instance, not a collaborator.
     */
    private boolean isSelfHierarchyDispatch(String callerMethod, String concreteSelfFqn) {
        if (!isPruningActive(concreteSelfFqn)) {
            return false;
        }
        String callerClass = classNameOf(callerMethod);
        return callerClass != null && context.isSubtypeOrSame(concreteSelfFqn, callerClass);
    }

    /**
     * An edge from an abstract/interface declaring type into one of its subtypes/implementations
     * is a virtual-dispatch fan-out (template-method hook expansion).
     */
    private boolean isVirtualDispatchCandidate(String callerMethod, String targetMethod) {
        String callerClass = classNameOf(callerMethod);
        String targetClass = classNameOf(targetMethod);
        if (callerClass == null || targetClass == null || callerClass.equals(targetClass)) {
            return false;
        }
        TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
        if (callerTd == null) {
            return false;
        }
        boolean callerPolymorphic = callerTd.isInterface() || Modifier.isAbstract(callerTd.getModifiers());
        if (!callerPolymorphic) {
            return false;
        }
        if (context.isSubtypeOrSame(targetClass, callerClass)) {
            return true;
        }
        List<String> impls = context.getImplementations(callerClass);
        return impls != null && impls.contains(targetClass);
    }

    private String nextConcreteSelf(String concreteSelfFqn, String callerMethod, String targetMethod) {
        // Only narrow self when dispatching within the entry hierarchy; never adopt a collaborator
        // impl type as the new "this".
        if (!isSelfHierarchyDispatch(callerMethod, concreteSelfFqn)
                || !isVirtualDispatchCandidate(callerMethod, targetMethod)) {
            return concreteSelfFqn;
        }
        String targetClass = classNameOf(targetMethod);
        if (targetClass != null && context.isConcreteClassType(targetClass)) {
            return targetClass;
        }
        return concreteSelfFqn;
    }

    private String nextConcreteSelfForImpl(
            String concreteSelfFqn, String abstractClassFqn, String implClassFqn) {
        if (!isPruningActive(concreteSelfFqn)
                || abstractClassFqn == null
                || !context.isSubtypeOrSame(concreteSelfFqn, abstractClassFqn)) {
            return concreteSelfFqn;
        }
        if (implClassFqn != null && context.isConcreteClassType(implClassFqn)) {
            return implClassFqn;
        }
        return concreteSelfFqn;
    }

    private static String memoKey(String start, String target, String concreteSelfFqn) {
        return start + "|" + target + "|" + Objects.toString(concreteSelfFqn, "");
    }

    private static String classNameOf(String methodFqn) {
        if (methodFqn == null || !methodFqn.contains(".")) {
            return null;
        }
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        if (className.contains("<")) {
            className = className.substring(0, className.indexOf('<'));
        }
        return className;
    }

    private static String methodNameOf(String methodFqn) {
        if (methodFqn == null || !methodFqn.contains(".")) {
            return methodFqn;
        }
        return methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
    }

    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;

        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;

            if (context.areSameTypeOrUnambiguousSimpleMatch(classNeighbor, classTarget)) return true;

            if (context.areClassesPolymorphicallyCompatible(classNeighbor, 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) {
            String leftClass = classNeighbor != null ? classNeighbor : simpleClassNeighbor;
            String rightClass = classTarget != null ? classTarget : simpleClassTarget;
            if (context.areSameTypeOrUnambiguousSimpleMatch(leftClass, rightClass)) return true;
            if (!context.isAmbiguousSimpleName(simpleClassNeighbor)
                    && !context.isAmbiguousSimpleName(simpleClassTarget)
                    && simpleClassNeighbor.equalsIgnoreCase(simpleClassTarget)) {
                return true;
            }
            // e.g. "this" vs "com.example.Machine"
            if (simpleClassNeighbor.equals("this") || simpleClassNeighbor.equals("super")) return true;
            return false;
        }

        // If we have no class ownership information for either side, we can't prove a match beyond the
        // method name. Fail closed to avoid accidental cross-class linking.
        return false;
    }

    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;
    }
}
