package ru.kirillius.pf.sdn.api.flow;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ru.kirillius.java.utils.events.ConcurrentEventHandler;
import ru.kirillius.java.utils.events.EventHandler;
import ru.kirillius.pf.sdn.api.Networking.NetworkScope;
import ru.kirillius.pf.sdn.entity.ActionConfig;
import ru.kirillius.pf.sdn.entity.ActionConnectionConfig;

import java.util.*;
import java.util.function.Function;

@Slf4j
@RequiredArgsConstructor
public class ExecutionGraph {

    public int size() {
        return nodes.size();
    }

    @Getter
    private final EventHandler<FlowAction> startExecutingEvent = new ConcurrentEventHandler<>();
    private final Map<ActionConfig, Node> nodes = new HashMap<>();

    private final Map<Node, CallContext> executed = new HashMap<>();

    public ExecutionGraph(List<ActionConfig> actionConfigs, List<ActionConnectionConfig> connectionConfigs, Function<ActionConfig, FlowAction> actionFactory) {
        var pendingActions = new ArrayList<>(actionConfigs);

        //создаём ноды у которых нет зависимостей
        actionConfigs
                .stream()
                .filter(config -> connectionConfigs
                        .stream()
                        .noneMatch(conn -> conn.getTo().equals(config))
                ).forEach(config -> {
                    nodes.put(config, new Node(actionFactory.apply(config), Collections.emptyList()));
                    pendingActions.remove(config);
                });
        //создаём оставшиеся ноды

        while (!pendingActions.isEmpty()) {
            //ищем конфиги, все соединения которых можно зарезолвить
            var spawned = new ArrayList<ActionConfig>();

            pendingActions.forEach((actionConfig) -> {
                var selfConnectionConfigs = connectionConfigs.stream().filter(conn -> conn.getTo().equals(actionConfig)).toList();
                if (selfConnectionConfigs.stream().allMatch(conn -> nodes.containsKey(conn.getFrom()))) {
                    spawned.add(actionConfig);

                    nodes.put(actionConfig,
                            new Node(
                                    actionFactory.apply(actionConfig),
                                    selfConnectionConfigs
                                            .stream()
                                            .map(connectionConfig -> new Node.Connection(
                                                    nodes.get(connectionConfig.getFrom()),
                                                    connectionConfig.getFromPort(),
                                                    connectionConfig.getToPort()
                                            )).toList()
                            )
                    );
                }
            });

            pendingActions.removeAll(spawned);

            if (spawned.isEmpty()) {
                throw new IllegalStateException("Connection loop detected and could not be resolved");
            }

        }

    }

    public Iterator<ExecutionResult> execute() {
        executed.clear();
        final var configs = new HashSet<>(nodes.keySet());

        return new Iterator<>() {
            @Override
            public boolean hasNext() {
                return executed.size() != nodes.size();
            }

            @Override
            public ExecutionResult next() {
                var nodeToExecute = findNodeToExecute(configs);
                var action = nodeToExecute.action();
                try {
                    startExecutingEvent.invoke(action);
                } catch (Exception e) {
                    log.error("Error on {}:startExecuting event", ExecutionGraph.class.getSimpleName(), e);
                }

                var inputs = new ArrayList<NetworkScope>();
                nodeToExecute.inputs().forEach((input) -> {
                    var fromNode = input.from();
                    inputs.add(executed.get(fromNode).getOutputs().get(input.fromPort));
                });

                var callContext = action.apply(inputs);
                executed.put(nodeToExecute, callContext);
                configs.remove(nodeToExecute.action().getConfig());

                return new ExecutionResult(action, callContext);
            }
        };
    }

    private Node findNodeToExecute(Set<ActionConfig> actionConfigs) {
        for (var config : actionConfigs) {
            var node = nodes.get(config);
            //проверяем что все на всех входах есть данные
            if (node.inputs().stream().allMatch(connection -> executed.containsKey(connection.from()))) {
                return node;
            }
        }

        throw new NoSuchElementException("There is No node to execute");
    }

    @Builder
    public record ExecutionResult(FlowAction action, CallContext context) {
    }

    private record Node(FlowAction action, List<Connection> inputs) {
        private record Connection(Node from, int fromPort, int toPort) {
        }
    }
}
