package de.avatic.lcc.service.calculation;

import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import org.jetbrains.annotations.Debug.Renderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * Service responsible for resolving and building chains of nodes.
 * <p>
 * This service class provides functionality to build all possible valid chains
 * starting from a specified node ID. It handles the validation and processing
 * of node chains, including detection of circular references.
 * </p>
 *
 */
@Service
public class ChainResolver {

    private static final Logger log = LoggerFactory.getLogger(ChainResolver.class);


    private final NodeRepository nodeRepository;

    /**
     * Constructs a new ChainResolver with the specified NodeRepository.
     *
     * @param nodeRepository The repository used to retrieve node chains
     */
    public ChainResolver(NodeRepository nodeRepository) {
        this.nodeRepository = nodeRepository;
    }


    /**
     * Builds all valid chains starting from the specified node ID.
     * <p>
     * This method retrieves all possible chains starting from the given node ID and processes
     * them to validate and build complete chains. It handles chain merging, validation, and
     * circular reference detection during the process.
     * </p>
     *
     * @param nodeId The ID of the starting node
     * @return A list of valid node chains, each represented as a list of Nodes
     */
    public List<List<Node>> buildChains(Integer nodeId) {
        if (nodeId == null)
            return Collections.emptyList();

        List<List<Node>> foundChains = new ArrayList<>();

        Stack<ChainValidationObject> chainStack = new Stack<>();
        chainStack.addAll(nodeRepository.getChainsById(nodeId).stream().map(ChainValidationObject::new).toList());

        while (!chainStack.isEmpty()) {
            var validationObject = chainStack.pop();

            if (!validationObject.checkCircularReference()) {
                while (validationObject.hasNext()) {
                    Node currentNode = validationObject.getCurrentNode();
                    chainStack.addAll(validationObject.validateChains(nodeRepository.getChainsById(currentNode.getId())));

                    if (!validationObject.valid())
                        break;

                    validationObject.next();
                }

                if (validationObject.valid() && !checkDeprecated(validationObject.getChain()) && checkIntermediate(validationObject.getChain()))
                    foundChains.add(new ArrayList<>(validationObject.getChain()));
            } else {
                log.warn("Circular reference detected while building predecessor chain for node {}", nodeId);
            }
        }

        log.info("Found {} chains for node {}", foundChains.size(), nodeId);
        log.info("Found chains: {}", foundChains);
        return foundChains;
    }

    private boolean checkIntermediate(List<Node> chain) {
        // remove chains if there is a node that cannot act as intermediate.
        return chain.stream().allMatch(Node::getIntermediate);
    }

    private boolean checkDeprecated(List<Node> chain) {
        return chain.stream().anyMatch(Node::getDeprecated);
    }

    /**
     * Helper class for chain validation and processing.
     * <p>
     * This inner class encapsulates the logic for validating node chains, handling
     * chain merging, and detecting circular references. It maintains the state of
     * chain processing including the current position and validity status.
     * </p>
     */
    @Renderer(text = "getFullChainView()")
    private static class ChainValidationObject {

        /**
         * Current position in the chain being processed
         */
        int chainPointer;

        /**
         * List of nodes representing the current chain
         */
        List<Node> chain;

        /**
         * Flag indicating if the chain is valid
         */
        private boolean chainValid;

        /**
         * Creates a new ChainValidationObject with the specified chain.
         *
         * @param chain The list of nodes representing the chain to validate
         */
        public ChainValidationObject(List<Node> chain) {
            this.chain = chain;
            this.chainPointer = 0;
            this.chainValid = true;
        }

        /**
         * Creates a new ChainValidationObject with the specified chain and pointer position.
         *
         * @param chain        The list of nodes representing the chain to validate
         * @param chainPointer The starting position in the chain
         */
        private ChainValidationObject(List<Node> chain, int chainPointer) {
            this.chain = chain;
            this.chainPointer = chainPointer;
            this.chainValid = true;
        }


        /**
         * Returns the current node in the chain based on the chainPointer.
         *
         * @return The current Node in the chain
         */
        public Node getCurrentNode() {
            return chain.get(chainPointer);
        }

        /**
         * Validates the given foreign chains against the current chain.
         * <p>
         * This method checks if the foreign chains can be merged with the current chain
         * starting from the current position. It handles various validation scenarios
         * including short chains and matching node IDs.
         * </p>
         *
         * @param foreignChains List of chains to validate against the current chain
         * @return Collection of ChainValidationObjects representing valid merged chains
         */
        public Collection<ChainValidationObject> validateChains(List<List<Node>> foreignChains) {

            if (foreignChains == null || foreignChains.isEmpty())
                return Collections.emptyList();

            var candidates = new ArrayList<>(foreignChains);
            var nextCandidates = new ArrayList<List<Node>>();
            boolean shortChainFound = false;

            /*
             * if we are at the end of the chain and all foreign chains are empty, there is no need to check any further
             * the chain object itself is a solution -> return empty.
             */

            if (chainPointer == chain.size() - 1 && foreignChains.stream().allMatch(List::isEmpty))
                return Collections.emptyList();

            int foreignIdx = 0;
            for (int localIdx = chainPointer + 1; localIdx < chain.size(); localIdx++, foreignIdx++) {
                var localNode = chain.get(localIdx);

                nextCandidates.clear();

                for (var candidate : candidates) {
                    if (candidate.size() == foreignIdx)
                        shortChainFound = true;
                    else {
                        Node foreignNode = candidate.get(foreignIdx);
                        if (Objects.equals(localNode.getId(), foreignNode.getId())) {
                            nextCandidates.add(candidate);
                        }
                    }

                }
                candidates.clear();
                candidates.addAll(nextCandidates);

                if (candidates.isEmpty())
                    break;
            }

            /*
             * if there are no candidates left, there is no need to check any further.
             * -> set chain to invalid and return empty
             * (if any of the chains ended before check was finished (== shortChainFound), the chain object itself is a
             * solution -> so keep the chain valid and return empty)
             */

            if (candidates.isEmpty()) {
                if (!shortChainFound)
                    this.chainValid = false;

                return Collections.emptyList();
            }

            return mergeCandidates(candidates, foreignIdx);
        }


        /**
         * Merges the current chain with candidate chains at the specified index.
         * <p>
         * Creates new ChainValidationObjects by merging the current chain with
         * each candidate chain, starting from the specified index in the candidate chains.
         * </p>
         *
         * @param candidates List of candidate chains to merge with
         * @param onIndex    The index in the candidate chains to start merging from
         * @return Collection of ChainValidationObjects representing the merged chains
         */
        private Collection<ChainValidationObject> mergeCandidates(List<List<Node>> candidates, int onIndex) {
            var mergedCandidates = new ArrayList<ChainValidationObject>();

            for (var candidate : candidates) {
                List<Node> mergedChain = new ArrayList<>(chain);
                mergedChain.addAll(candidate.subList(onIndex, candidate.size()));

                var chainObj = new ChainValidationObject(mergedChain, onIndex);
                if(!chainEquals(chainObj) && mergedCandidates.stream().noneMatch(chainObj::chainEquals))
                    mergedCandidates.add(chainObj);
            }

            return mergedCandidates;
        }

        public boolean chainEquals(ChainValidationObject other) {
            return chain.size() == other.chain.size() && IntStream.range(0, chain.size()).allMatch(i -> chain.get(i).getId().equals(other.chain.get(i).getId()));
        }

        /**
         * Checks if there are more nodes in the chain after the current position.
         *
         * @return true if there are more nodes in the chain, false otherwise
         */
        public boolean hasNext() {
            return chainPointer < chain.size();
        }

        /**
         * Advances the chain pointer to the next position.
         */
        public void next() {
            chainPointer++;
        }

        /**
         * Returns the validity status of the chain.
         *
         * @return true if the chain is valid, false otherwise
         */
        public boolean valid() {
            return chainValid;
        }

        /**
         * Returns the current chain of nodes.
         *
         * @return List of Nodes representing the current chain
         */
        public List<Node> getChain() {
            return chain;
        }

        /**
         * Checks if the chain contains circular references.
         * <p>
         * A circular reference occurs when the same node ID appears multiple times
         * within the chain.
         * </p>
         *
         * @return true if a circular reference is detected, false otherwise
         */
        public boolean checkCircularReference() {
            /* check for circular references */
            return (chain.stream().map(Node::getId)
                    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
                    .entrySet().stream()
                    .anyMatch(entry -> entry.getValue() > 1));
        }


        private String getFullChainView() {
            StringBuilder sb = new StringBuilder();
            sb.append(chainValid ? "✓ " : "✗ ");
            sb.append("Chain[").append(chainPointer).append("|").append(chain.size()).append("]: ");

            for (int i = 0; i < Math.min(chain.size(), 5); i++) {
                if (i > 0) sb.append(" -> ");
                if (i == chainPointer) sb.append("[");
                sb.append(chain.get(i).getExternalMappingId());
                if (i == chainPointer) sb.append("]");
            }

            if (chain.size() > 5) sb.append(" ...");
            return sb.toString();
        }
    }


}
