package de.avatic.lcc.service.transformer.generic;

import de.avatic.lcc.dto.generic.NodeDTO;
import de.avatic.lcc.dto.generic.NodeType;
import de.avatic.lcc.model.excel.ExcelNode;
import de.avatic.lcc.model.db.country.Country;
import de.avatic.lcc.model.db.country.IsoCode;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.model.db.premises.route.RouteNode;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class NodeTransformer {

    private final CountryTransformer countryTransformerService;
    private final CountryRepository countryRepository;
    private final LocationTransformer locationTransformer;
    private final NodeRepository nodeRepository;

    public NodeTransformer(CountryTransformer countryTransformerService, CountryRepository countryRepository, LocationTransformer locationTransformer, NodeRepository nodeRepository) {
        this.countryTransformerService = countryTransformerService;
        this.countryRepository = countryRepository;
        this.locationTransformer = locationTransformer;
        this.nodeRepository = nodeRepository;
    }

    public NodeDTO toNodeDTO(Node entity) {
        NodeDTO dto = new NodeDTO();

        ArrayList<NodeType> types = new ArrayList<>();
        if (entity.getDestination()) types.add(NodeType.DESTINATION);
        if (entity.getSource()) types.add(NodeType.SOURCE);
        if (entity.getIntermediate()) types.add(NodeType.INTERMEDIATE);

        dto.setId(entity.getId());
        dto.setName(entity.getName());
        dto.setAddress(entity.getAddress());
        dto.setCountry(countryTransformerService.toCountryDTO(countryRepository.getById(entity.getCountryId()).orElseThrow()));
        dto.setTypes(types);
        dto.setDeprecated(entity.getDeprecated());
        dto.setLocation(locationTransformer.toLocationDTO(entity));
        dto.setUserNode(false);
        dto.setExternalMappingId(entity.getExternalMappingId());

        return dto;
    }

    public NodeDTO toUserNodeDTO(Node entity) {
        NodeDTO dto = new NodeDTO();

        dto.setId(entity.getId());
        dto.setName(entity.getName());
        dto.setAddress(entity.getAddress());
        dto.setCountry(countryTransformerService.toCountryDTO(countryRepository.getById(entity.getCountryId()).orElseThrow()));
        dto.setTypes(List.of(NodeType.SOURCE));
        dto.setDeprecated(entity.getDeprecated());
        dto.setLocation(locationTransformer.toLocationDTO(entity));
        dto.setUserNode(true);
        dto.setExternalMappingId(entity.getExternalMappingId());

        return dto;
    }

    public NodeDTO toNodeDTO(RouteNode entity) {
        NodeDTO dto = new NodeDTO();

        ArrayList<NodeType> types = new ArrayList<>();
        if (entity.getDestination()) types.add(NodeType.DESTINATION);
        if (entity.getSource()) types.add(NodeType.SOURCE);
        if (entity.getIntermediate()) types.add(NodeType.INTERMEDIATE);

        dto.setId(entity.getId());
        dto.setName(entity.getName());
        dto.setAddress(entity.getAddress());
        dto.setCountry(countryTransformerService.toCountryDTO(countryRepository.getById(entity.getCountryId()).orElseThrow()));
        dto.setTypes(types);
        dto.setDeprecated(entity.getOutdated());
        dto.setLocation(locationTransformer.toLocationDTO(entity));
        dto.setUserNode(false);
        dto.setExternalMappingId(entity.getExternalMappingId());

        return dto;
    }

    public Node toNodeEntity(ExcelNode excelNode) {
        Node entity = new Node();

        var countryId = countryRepository.getByIsoCode(excelNode.getCountryId());

        var isoCodes = excelNode.getOutboundCountries().stream().map(IsoCode::valueOf).toList();
        var outboundCountry = countryRepository.getByIsoCodes(isoCodes);

        var unknownCountries = isoCodes.stream().filter(c -> outboundCountry.stream().noneMatch(o -> o.getIsoCode().equals(c))).toList();
        if (!unknownCountries.isEmpty()) {
            throw new ExcelValidationError("Outbound country list contains unknown country code:" + unknownCountries);
        }


        if (countryId.isEmpty()) {
            throw new ExcelValidationError("Country with iso code " + excelNode.getCountryId() + " does not exist.");
        }

        entity.setId(excelNode.getId());
        entity.setExternalMappingId(excelNode.getExternalMappingId());

        entity.setName(excelNode.getName());
        entity.setAddress(excelNode.getAddress());
        entity.setCountryId(countryId.get().getId());

        entity.setGeoLat(excelNode.getGeoLat());
        entity.setGeoLng(excelNode.getGeoLng());

        entity.setSource(excelNode.getSource());
        entity.setIntermediate(excelNode.getIntermediate());
        entity.setDestination(excelNode.getDestination());

        entity.setPredecessorRequired(excelNode.getPredecessorRequired());
        entity.setDeprecated(false);
        entity.setOutboundCountries(outboundCountry.stream().map(Country::getId).toList());
        entity.setNodePredecessors(convertPredecessors(excelNode.getNodePredecessors()));

        checkPredecessors(excelNode, entity.getNodePredecessors());

        return entity;
    }

    private List<Map<Integer, Integer>> convertPredecessors(List<List<String>> mappingIdLists) {
        var predecessorLists = new ArrayList<Map<Integer, Integer>>();
        var nodeIds = nodeRepository.getIdsByExternalMappingIds(mappingIdLists.stream().flatMap(List::stream).distinct().toList());

        for (var mappingIdList : mappingIdLists) {

            var predecessorList = new HashMap<Integer, Integer>();
            int predecessorIndex = 1;

            for (String mappingId : mappingIdList) {
                var nodeId = nodeIds.get(mappingId);
                if (nodeId == null) {
                    throw new ExcelValidationError("Predecessor list contains unknown node id: " + mappingId + ".");
                } else {
                    predecessorList.put(predecessorIndex++, nodeId);
                }
            }

            predecessorLists.add(predecessorList);
        }

        return predecessorLists;
    }

    private void checkPredecessors(ExcelNode excelNode, List<Map<Integer, Integer>> predecessorLists) {
        predecessorLists.forEach(predecessorList -> {

            if(predecessorList.containsValue(excelNode.getId()))
                throw new ExcelValidationError("Predecessor list of node " + excelNode.getExternalMappingId() + " contains itself.");

            var duplicates = findDuplicates(predecessorList.values());
            if(!duplicates.isEmpty()) {
                throw new ExcelValidationError(String.format("Predecessor list of node '%s' contains loop caused by: %s", excelNode.getExternalMappingId(), duplicates) );
            }
        });
    }

    public static <T> Set<T> findDuplicates(Collection<T> collection) {
        Set<T> seen = new HashSet<>();
        Set<T> duplicates = new HashSet<>();

        for (T item : collection) {
            if (!seen.add(item)) {
                duplicates.add(item);
            }
        }
        return duplicates;
    }

}
