package de.avatic.lcc.service.access;

import de.avatic.lcc.dto.calculation.DestinationDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.model.db.premises.Premise;
import de.avatic.lcc.model.db.premises.route.*;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.premise.*;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.calculation.RoutingService;
import de.avatic.lcc.service.transformer.premise.DestinationTransformer;
import de.avatic.lcc.service.users.AuthorizationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class DestinationService {


    private final DestinationRepository destinationRepository;
    private final DestinationTransformer destinationTransformer;
    private final RouteRepository routeRepository;
    private final RouteSectionRepository routeSectionRepository;
    private final RouteNodeRepository routeNodeRepository;
    private final RoutingService routingService;
    private final NodeRepository nodeRepository;
    private final PremiseRepository premiseRepository;
    private final UserNodeRepository userNodeRepository;
    private final AuthorizationService authorizationService;

    public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService) {
        this.destinationRepository = destinationRepository;
        this.destinationTransformer = destinationTransformer;
        this.routeRepository = routeRepository;
        this.routeSectionRepository = routeSectionRepository;
        this.routeNodeRepository = routeNodeRepository;
        this.routingService = routingService;
        this.nodeRepository = nodeRepository;
        this.premiseRepository = premiseRepository;
        this.userNodeRepository = userNodeRepository;
        this.authorizationService = authorizationService;
    }


    private Map<Integer, List<Destination>> processDestinations(List<Premise> premisesToProcess, Map<Integer, List<Integer>> destinationNodeIds, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost, Map<RouteIds, List<RouteInformation>> routes) {

        var destMap = new HashMap<Integer, List<Destination>>();

        for (var premise : premisesToProcess) {

            var destinations = new ArrayList<Destination>();

            for (var destinationNodeId : destinationNodeIds.get(premise.getId())) {

                Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow();

                var destination = new Destination();
                destination.setDestinationNodeId(destinationNodeId);
                destination.setPremiseId(premise.getId());
                destination.setAnnualAmount(annualAmount);
                destination.setD2d(false);
                destination.setLeadTimeD2d(null);
                destination.setRateD2d(null);
                destination.setDisposalCost(disposalCost == null ? null : BigDecimal.valueOf(disposalCost.doubleValue()));
                destination.setHandlingCost(handlingCost == null ? null : BigDecimal.valueOf(handlingCost.doubleValue()));
                destination.setRepackingCost(repackingCost == null ? null : BigDecimal.valueOf(repackingCost.doubleValue()));
                destination.setCountryId(destinationNode.getCountryId());
                destination.setGeoLat(destinationNode.getGeoLat());
                destination.setGeoLng(destinationNode.getGeoLng());
                destination.setId(destinationRepository.insert(destination));

                Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();

                if (routes != null)
                    //noinspection SpringTransactionalMethodCallsInspection
                    saveRoute(routes.get(new RouteIds(source.getId(), destinationNodeId, premise.getSupplierNodeId() == null)), destination.getId());


                destinations.add(destination);
            }

            destMap.put(premise.getId(), destinations);
        }

        return destMap;
    }

    @Transactional
    public Map<Integer, List<DestinationDTO>> massSetDestinationProperties(DestinationSetDTO dto) {
        var admin = authorizationService.isSuper();
        Integer userId = authorizationService.getUserId();

        if (!admin)
            destinationRepository.checkOwner(dto.getPremiseId(), userId);

        deleteAllDestinationsByPremiseId(dto.getPremiseId(), false);

        var premisses = premiseRepository.getPremisesById(dto.getPremiseId());
        // TODO no routing in set ... only props.
//        Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, dto.getDestinations().stream().map(DestinationSetListItemDTO::getDestinationNodeId).toList());

//        Map<Integer, List<Destination>> destinations = dto.getDestinations().stream()
//                .flatMap(destination -> processDestinations(premisses, destination.getDestinationNodeId(), destination.getAnnualAmount(), destination.getRepackingCost(), destination.getDisposalCost(), destination.getHandlingCost(), null).stream())
//                .collect(Collectors.groupingBy(Destination::getPremiseId));


//        return destinations.entrySet().stream()
//                .collect(Collectors.toMap(
//                        Map.Entry::getKey,
//                        entry -> entry.getValue().stream().map(destinationTransformer::toDestinationDTO).toList()));

        return null;
    }

    private List<Integer> getDestinationToRemove(List<Destination> oldDestinations, List<Integer> newIds) {
        return oldDestinations.stream().filter(dest -> !newIds.contains(dest.getDestinationNodeId())).map(Destination::getId).collect(Collectors.toList());
    }

    private List<Integer> getNodeIdsToAdd(List<Destination> oldDestinations, List<Integer> newIds) {
        var oldIds = oldDestinations.stream().map(Destination::getDestinationNodeId).toList();
        return newIds.stream().filter(id -> !oldIds.contains(id)).collect(Collectors.toList());
    }

    @Transactional
    public Map<Integer, List<DestinationDTO>> massSetDestinations(DestinationCreateDTO dto) {

        Integer userId = authorizationService.getUserId();

        Map<Integer, List<Integer>> destinationsToProcess = new HashMap<>();

        var requestedDestMap = dto.getDestinationNodeIds();
        var existingDestMap = dto.getDestinationNodeIds().keySet().stream().collect(Collectors.toMap(id -> id, id -> destinationRepository.getByPremiseIdAndUserId(id, userId)));

        for (Integer premiseId : requestedDestMap.keySet()) {
            var requestedDestinations = requestedDestMap.get(premiseId);
            var existingDestinations = existingDestMap.getOrDefault(premiseId, Collections.emptyList());

            /* remove deselected */
            var toRemove = getDestinationToRemove(existingDestinations, requestedDestinations);
            deleteDestinationsById(toRemove, false);

            /* find new selected */
            var toAdd = getNodeIdsToAdd(existingDestinations, requestedDestinations);
            destinationsToProcess.put(premiseId, toAdd);
        }

        if (destinationsToProcess.isEmpty())
            return new HashMap<>();

        var premisses = premiseRepository.getPremisesById(new ArrayList<>(destinationsToProcess.keySet()));
        Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, destinationsToProcess);


        var destinations = processDestinations(premisses, destinationsToProcess, null, null, null, null, routes);
        return destinations.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> entry.getValue().stream()
                                .map(destinationTransformer::toDestinationDTO)
                                .toList()
                ));
    }

    public DestinationDTO getDestination(Integer id) {
        var admin = authorizationService.isSuper();
        var userId = authorizationService.getUserId();

        if (!admin)
            destinationRepository.checkOwner(id, userId);

        return destinationTransformer.toDestinationDTO(destinationRepository.getById(id).orElseThrow());
    }

    @Transactional
    public void updateDestination(Integer id, DestinationUpdateDTO destinationUpdateDTO) {
        var admin = authorizationService.isSuper();
        Integer userId = authorizationService.getUserId();

        if (!admin)
            destinationRepository.checkOwner(id, userId);

        var selectedRouteId = destinationUpdateDTO.getRouteSelectedId();

        if (selectedRouteId != null) {
            routeRepository.updateSelectedByDestinationId(id, selectedRouteId);
        }

        destinationRepository.update(id,
                destinationUpdateDTO.getAnnualAmount(),
                destinationUpdateDTO.getRepackingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRepackingCost().doubleValue()),
                destinationUpdateDTO.getDisposalCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getDisposalCost().doubleValue()),
                destinationUpdateDTO.getHandlingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getHandlingCost().doubleValue()),
                destinationUpdateDTO.getD2d(), destinationUpdateDTO.getRateD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRateD2d().doubleValue()),
                destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d())
        );

    }

    private Map<RouteIds, List<RouteInformation>> findRoutes(List<Premise> premisses, Map<Integer, List<Integer>> routingRequest) {

        Map<RouteIds, List<RouteInformation>> routes = new HashMap<>();
        Map<Integer, Node> nodes = new HashMap<>();
        Map<Integer, Node> userNodes = new HashMap<>();

        for (var premise : premisses) {

            for (var destinationId : routingRequest.get(premise.getId())) {

                boolean isUserSupplierNode = (premise.getSupplierNodeId() == null);
                var ids = new RouteIds(isUserSupplierNode ? premise.getUserSupplierNodeId() : premise.getSupplierNodeId(), destinationId, isUserSupplierNode);
                if (routes.containsKey(ids)) continue;

                if (!nodes.containsKey(destinationId)) {
                    nodes.put(destinationId, nodeRepository.getById(destinationId).orElseThrow());
                }

                if (!isUserSupplierNode && !nodes.containsKey(premise.getSupplierNodeId())) {
                    nodes.put(premise.getSupplierNodeId(), nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow());
                }

                if (isUserSupplierNode && !userNodes.containsKey(premise.getUserSupplierNodeId())) {
                    userNodes.put(premise.getUserSupplierNodeId(), userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow());
                }


                //TODO in parallel
                routes.put(ids, routingService.findRoutes(nodes.get(destinationId), isUserSupplierNode ? userNodes.get(premise.getUserSupplierNodeId()) : nodes.get(premise.getSupplierNodeId()), isUserSupplierNode));
            }
        }

        return routes;
    }

    @Transactional
    public void saveRoute(List<RouteInformation> routeObjs, Integer destinationId) {

        for (var routeObj : routeObjs) {
            boolean first = true;
            Integer fromNodeId = null;

            var premiseRoute = routeObj.getRoute();
            premiseRoute.setDestinationId(destinationId);
            int routeId = routeRepository.insert(premiseRoute);


            for (RouteSectionInformation section : routeObj.getSections()) {

                if (first) {
                    fromNodeId = routeNodeRepository.insert(section.getFromNode());
                    first = false;
                }

                var toNode = section.getToNode();
                Integer toNodeId = routeNodeRepository.insert(toNode);

                var premiseRouteSection = section.getSection();

                premiseRouteSection.setRouteId(routeId);
                premiseRouteSection.setFromRouteNodeId(fromNodeId);
                premiseRouteSection.setToRouteNodeId(toNodeId);

                routeSectionRepository.insert(premiseRouteSection);

                fromNodeId = toNodeId;
            }
        }
    }

    @Transactional
    public void findRouteAndSave(Integer destinationId, Node destination, Node supplier, boolean isUserSupplierNode) {

        var routeObjs = routingService.findRoutes(destination, supplier, isUserSupplierNode);

        saveRoute(routeObjs, destinationId);
    }

    @Transactional
    public void deleteAllDestinationsByPremiseId(List<Integer> ids, boolean deleteRoutesOnly) {
        ids.forEach(id -> deleteAllDestinationsByPremiseId(id, deleteRoutesOnly));
    }

    @Transactional
    public void deleteAllDestinationsByPremiseId(Integer premiseId, boolean deleteRoutesOnly) {
        var destinations = destinationRepository.getByPremiseId(premiseId);
        destinations.forEach(destination -> deleteDestinationById(destination.getId(), deleteRoutesOnly));
    }

    @Transactional
    public void deleteDestinationsById(List<Integer> ids, boolean deleteRoutesOnly) {
        ids.forEach(id -> deleteDestinationById(id, deleteRoutesOnly));
    }

    @Transactional
    public void deleteDestinationById(Integer id, boolean deleteRoutesOnly) {
        var admin = authorizationService.isSuper();
        Integer userId = authorizationService.getUserId();

        if (!admin)
            destinationRepository.checkOwner(id, userId);

        List<Route> routes = routeRepository.getByDestinationId(id);

        for (var route : routes) {
            List<RouteSection> sections = routeSectionRepository.getByRouteId(route.getId());
            routeSectionRepository.deleteAllById(sections.stream().map(RouteSection::getId).toList());
            routeNodeRepository.deleteAllById(sections.stream().flatMap(section -> Stream.of(section.getFromRouteNodeId(), section.getToRouteNodeId())).toList());
        }

        routeRepository.deleteAllById(routes.stream().map(Route::getId).toList());

        if (!deleteRoutesOnly)
            destinationRepository.deleteById(id);

    }

    @Transactional
    public void duplicate(Integer fromPremiseId, Integer toPremiseId) {

        List<Destination> destinations = destinationRepository.getByPremiseId(fromPremiseId);

        for (Destination destination : destinations) {

            destination.setPremiseId(toPremiseId);
            Integer destinationId = destinationRepository.insert(destination);

            for (Route route : routeRepository.getByDestinationId(destination.getId())) {
                route.setDestinationId(destinationId);
                Integer routeId = routeRepository.insert(route);

                for (RouteSection section : routeSectionRepository.getByRouteId(route.getId())) {
                    section.setRouteId(routeId);

                    RouteNode fromNode = routeNodeRepository.getById(section.getFromRouteNodeId()).orElseThrow();
                    RouteNode toNode = routeNodeRepository.getById(section.getToRouteNodeId()).orElseThrow();

                    Integer fromNodeId = routeNodeRepository.insert(fromNode);
                    Integer toNodeId = routeNodeRepository.insert(toNode);

                    section.setFromRouteNodeId(fromNodeId);
                    section.setToRouteNodeId(toNodeId);

                    routeSectionRepository.insert(section);
                }
            }
        }
    }

    private record RouteIds(Integer fromNodeId, Integer toNodeId, boolean isUserSupplierNode) {
    }


}
