package com.graphtest;

import com.graphtest.model.Edge;
import com.graphtest.model.EdgeType;
import com.graphtest.model.Node;
import com.graphtest.model.NodeStatus;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CodeGraph {
    private final Map<String, Node> nodes = new LinkedHashMap<>();
    private final Set<Edge> edges = new LinkedHashSet<>();
    private final List<String> buildErrors = new ArrayList<>();
    private final List<List<String>> detectedCycles = new ArrayList<>();

    public void addNode(Node node) {
        nodes.putIfAbsent(node.getClassName(), node);
    }

    public void addEdge(Edge edge) {
        edges.add(edge);
    }

    public void addBuildError(String error) {
        buildErrors.add(error);
    }

    public Node getNode(String className) {
        return nodes.get(className);
    }

    public boolean hasNode(String className) {
        return nodes.containsKey(className);
    }

    public boolean hasEdge(String sourceClass, String targetClass) {
        return edges.stream().anyMatch(e ->
            e.getSource().getClassName().equals(sourceClass) &&
            e.getTarget().getClassName().equals(targetClass));
    }

    public boolean hasEdge(String sourceClass, String targetClass, EdgeType type) {
        return edges.stream().anyMatch(e ->
            e.getSource().getClassName().equals(sourceClass) &&
            e.getTarget().getClassName().equals(targetClass) &&
            e.getEdgeType() == type);
    }

    public List<Edge> getSelfLoops() {
        return edges.stream().filter(Edge::isSelfLoop).collect(Collectors.toList());
    }

    public List<Edge> getOutgoingEdges(String className) {
        return edges.stream()
            .filter(e -> e.getSource().getClassName().equals(className))
            .collect(Collectors.toList());
    }

    public List<Edge> getIncomingEdges(String className) {
        return edges.stream()
            .filter(e -> e.getTarget().getClassName().equals(className) && !e.isSelfLoop())
            .collect(Collectors.toList());
    }

    public List<Edge> getEdgesByType(EdgeType type) {
        return edges.stream()
            .filter(e -> e.getEdgeType() == type)
            .collect(Collectors.toList());
    }

    public List<Node> getNodesByStatus(NodeStatus status) {
        return nodes.values().stream()
            .filter(n -> n.getStatus() == status)
            .collect(Collectors.toList());
    }

    public List<Node> getRootNodes() {
        Set<String> hasIncoming = edges.stream()
            .filter(e -> !e.isSelfLoop())
            .map(e -> e.getTarget().getClassName())
            .collect(Collectors.toSet());
        return nodes.values().stream()
            .filter(n -> n.getStatus() == NodeStatus.OK)
            .filter(n -> !hasIncoming.contains(n.getClassName()))
            .collect(Collectors.toList());
    }

    public List<Node> getLeafNodes() {
        Set<String> hasOutgoing = edges.stream()
            .filter(e -> !e.isSelfLoop())
            .map(e -> e.getSource().getClassName())
            .collect(Collectors.toSet());
        return nodes.values().stream()
            .filter(n -> n.getStatus() == NodeStatus.OK)
            .filter(n -> !hasOutgoing.contains(n.getClassName()))
            .collect(Collectors.toList());
    }

    public List<Node> getIsolatedNodes() {
        Set<String> connected = edges.stream()
            .flatMap(e -> Stream.of(e.getSource().getClassName(), e.getTarget().getClassName()))
            .collect(Collectors.toSet());
        return nodes.values().stream()
            .filter(n -> !connected.contains(n.getClassName()))
            .collect(Collectors.toList());
    }

    public Map<String, Node> getNodes() { return Collections.unmodifiableMap(nodes); }
    public Set<Edge> getEdges() { return Collections.unmodifiableSet(edges); }
    public List<String> getBuildErrors() { return Collections.unmodifiableList(buildErrors); }
    public List<List<String>> getDetectedCycles() { return Collections.unmodifiableList(detectedCycles); }

    public void detectCycles() {
        detectedCycles.clear();
        Set<String> visited = new HashSet<>();
        Set<String> inStack = new HashSet<>();
        for (String node : nodes.keySet()) {
            if (!visited.contains(node)) {
                dfs(node, visited, inStack, new ArrayList<>());
            }
        }
    }

    private void dfs(String current, Set<String> visited, Set<String> inStack, List<String> path) {
        visited.add(current);
        inStack.add(current);
        path.add(current);

        for (Edge edge : edges) {
            if (!edge.isSelfLoop() && edge.getSource().getClassName().equals(current)) {
                String neighbor = edge.getTarget().getClassName();
                if (!visited.contains(neighbor)) {
                    dfs(neighbor, visited, inStack, path);
                } else if (inStack.contains(neighbor)) {
                    int cycleStart = path.indexOf(neighbor);
                    List<String> cycle = new ArrayList<>(path.subList(cycleStart, path.size()));
                    cycle.add(neighbor);
                    if (!isCycleAlreadyDetected(cycle)) {
                        detectedCycles.add(cycle);
                    }
                }
            }
        }

        inStack.remove(current);
        path.remove(path.size() - 1);
    }

    private boolean isCycleAlreadyDetected(List<String> newCycle) {
        Set<String> newSet = new HashSet<>(newCycle);
        return detectedCycles.stream().anyMatch(c -> new HashSet<>(c).equals(newSet));
    }
}
