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.nodes.Node;
import de.avatic.lcc.model.premises.route.*;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.premise.*;
import de.avatic.lcc.repositories.properties.PropertyRepository;
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.util.exception.base.ForbiddenException;
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 PropertyRepository propertyRepository;
    private final PropertyService propertyService;

    public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, PropertyRepository propertyRepository, PropertyService propertyService) {
        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.propertyRepository = propertyRepository;
        this.propertyService = propertyService;
    }

    @Transactional
    public Map<Integer, List<DestinationDTO>> setDestination(DestinationSetDTO dto) {
        //TODO fix user authorization
        var userId = 1;
        //dto.getPremiseId().forEach(id -> destinationRepository.checkOwner(id, userId));

        deleteAllDestinationsByPremiseId(dto.getPremiseId(), false);

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


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


    private List<Destination> createDestination(List<Integer> premiseIds, Integer destinationNodeId, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost) {

        var premisesToProcess = premiseRepository.getPremisesById(premiseIds);
        Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow();

        var destinations = new ArrayList<Destination>();

        for (var premise : premisesToProcess) {
            var destination = new Destination();
            destination.setDestinationNodeId(destinationNodeId);
            destination.setPremiseId(premise.getId());
            destination.setAnnualAmount(annualAmount == null ? 0 : 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();
            findRouteAndSave(destination.getId(), destinationNode, source, premise.getSupplierNodeId() == null);
            destinations.add(destination);
        }

        return destinations;

    }

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

        Integer userId = 1; //TODO get user id.

        var existingDestinations = destinationRepository.getByPremiseIdsAndNodeId(dto.getPremiseId(), dto.getDestinationNodeId(), userId);

        var premisesIdsToProcess = new ArrayList<Integer>();
        for (var premiseId : dto.getPremiseId()) {
            if (existingDestinations.stream().map(Destination::getPremiseId).noneMatch(id -> id.equals(premiseId))) {
                premisesIdsToProcess.add(premiseId);
            }
        }

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


        var destinations = createDestination(premisesIdsToProcess, dto.getDestinationNodeId(), null, null, null, null);
        return destinations.stream().collect(Collectors.toMap(Destination::getPremiseId, destinationTransformer::toDestinationDTO));

    }

    public DestinationDTO getDestination(Integer id) {
        //todo check authorization

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

    @Transactional
    public void updateDestination(Integer id, DestinationUpdateDTO destinationUpdateDTO) {
        //todo check authorization
        Integer userId = 1;
        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()));
    }


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

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

        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 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 deleteDestinationById(Integer id, boolean deleteRoutesOnly) {
        //todo check authorization
        Integer userId = 1;
        Optional<Integer> ownerId = destinationRepository.getOwnerIdById(id);

        if (userId == 1 /* todo remove */ || ownerId.isPresent() && ownerId.get().equals(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);

            return;
        }

        throw new ForbiddenException("Not authorized to delete destination with id " + 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);
                }
            }
        }
    }


}
