package ru.yandex.qe.dispenser.ws.api.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.inject.Inject;

import com.google.common.collect.Sets;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.QuotaChangeRequest;
import ru.yandex.qe.dispenser.domain.Service;
import ru.yandex.qe.dispenser.domain.d.DeliveryAccountDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryAccountsSpaceDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryDestinationDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryDestinationProvidersDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryDestinationRequestDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryDestinationResourceDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryDestinationResponseDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryProviderDto;
import ru.yandex.qe.dispenser.domain.d.DeliveryResourcesAccountsDto;
import ru.yandex.qe.dispenser.domain.dao.quota.request.QuotaChangeRequestDao;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.hierarchy.HierarchySupplier;
import ru.yandex.qe.dispenser.domain.i18n.LocalizableString;
import ru.yandex.qe.dispenser.domain.resources_model.ExternalDeliverableResource;
import ru.yandex.qe.dispenser.domain.resources_model.QuotaChangeRequestsDeliveryMappings;
import ru.yandex.qe.dispenser.domain.resources_model.QuotaChangeRequestDeliveryMapping;
import ru.yandex.qe.dispenser.domain.resources_model.DeliverableResource;
import ru.yandex.qe.dispenser.domain.util.LocalizationUtils;
import ru.yandex.qe.dispenser.ws.ResourceRequestAllocationManager;
import ru.yandex.qe.dispenser.ws.api.model.distribution.ValidatedAllocationDestinationSelectionQuotaRequest;
import ru.yandex.qe.dispenser.ws.api.model.distribution.ValidatedAllocationDestinationSelectionRequest;
import ru.yandex.qe.dispenser.ws.common.domain.errors.ErrorCollection;
import ru.yandex.qe.dispenser.ws.common.domain.errors.TypedError;
import ru.yandex.qe.dispenser.ws.common.domain.result.Result;
import ru.yandex.qe.dispenser.ws.d.DApiHelper;
import ru.yandex.qe.dispenser.ws.intercept.SessionInitializer;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.context.PerformerContext;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationGroup;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationProvider;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationQuotaRequest;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationSelectionQuotaRequest;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationSelectionRequest;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationSelectionResponse;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationSourceResource;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationSourceResourceSegment;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationTargetAccount;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationTargetResource;
import ru.yandex.qe.dispenser.ws.resources_model.ResourcesModelMapper;
import ru.yandex.qe.dispenser.ws.resources_model.ResourcesModelMapperManager;

@Component
public class AllocationDestinationsManager {

    private final MessageSource errorMessageSource;
    private final HierarchySupplier hierarchySupplier;
    private final QuotaChangeRequestDao quotaChangeRequestDao;
    private final ResourcesModelMapperManager resourcesModelMapperManager;
    private final DApiHelper dApiHelper;

    @Inject
    public AllocationDestinationsManager(@Qualifier("errorMessageSource") MessageSource errorMessageSource,
                                         HierarchySupplier hierarchySupplier,
                                         QuotaChangeRequestDao quotaChangeRequestDao,
                                         ResourcesModelMapperManager resourcesModelMapperManager,
                                         DApiHelper dApiHelper) {
        this.errorMessageSource = errorMessageSource;
        this.hierarchySupplier = hierarchySupplier;
        this.quotaChangeRequestDao = quotaChangeRequestDao;
        this.resourcesModelMapperManager = resourcesModelMapperManager;
        this.dApiHelper = dApiHelper;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public Result<AllocationDestinationSelectionResponse, ErrorCollection<String, TypedError<String>>> find(
            AllocationDestinationSelectionRequest request,
            PerformerContext performerContext,
            Locale locale) {
        return validate(request, performerContext, locale).apply(r -> doFind(r, locale));
    }

    private Result<ValidatedAllocationDestinationSelectionRequest, ErrorCollection<String, TypedError<String>>>
        validate(AllocationDestinationSelectionRequest request, PerformerContext performerContext, Locale locale) {
        Hierarchy hierarchy = hierarchySupplier.get();
        ErrorCollection.Builder<String, TypedError<String>> errorsBuilder = ErrorCollection.builder();
        ValidatedAllocationDestinationSelectionRequest.Builder builder
                = ValidatedAllocationDestinationSelectionRequest.builder();
        builder.author(performerContext.getPerson());
        if (request.getRequests().isEmpty()) {
            addFieldValidationError(errorsBuilder, "requests", locale, "field.is.required");
        }
        List<Long> quotaRequestIds = new ArrayList<>();
        Set<String> providerKeys = new HashSet<>();
        for (int i = 0; i < request.getRequests().size(); i++) {
            AllocationDestinationSelectionQuotaRequest quotaRequestInput = request.getRequests().get(i);
            if (quotaRequestInput == null) {
                addFieldValidationError(errorsBuilder, "requests." + i, locale, "field.is.required");
                continue;
            }
            quotaRequestIds.add(quotaRequestInput.getQuotaRequestId());
            List<String> currentProviderKeys = new ArrayList<>();
            for (int j = 0; j < quotaRequestInput.getProviderKeys().size(); j++) {
                String providerKey = quotaRequestInput.getProviderKeys().get(j);
                if (providerKey == null) {
                    addFieldValidationError(errorsBuilder, "requests." + i + ".providerKeys." + j, locale,
                            "field.is.required");
                    continue;
                }
                providerKeys.add(providerKey);
                currentProviderKeys.add(providerKey);
            }
            if (currentProviderKeys.size() != new HashSet<>(currentProviderKeys).size()) {
                addFieldValidationError(errorsBuilder, "requests." + i + ".providerKeys", locale,
                        "non.unique.elements");
            }
        }
        Set<Long> distinctQuotaRequestIds = new HashSet<>(quotaRequestIds);
        if (distinctQuotaRequestIds.size() != quotaRequestIds.size()) {
            addFieldValidationError(errorsBuilder, "requests", locale, "non.unique.elements");
        }
        Map<String, Service> providersByKey = hierarchy.getServiceReader().readPresent(providerKeys);
        Map<Long, QuotaChangeRequest> requestsById = quotaChangeRequestDao.read(new HashSet<>(distinctQuotaRequestIds));
        for (int i = 0; i < request.getRequests().size(); i++) {
            AllocationDestinationSelectionQuotaRequest quotaRequestInput = request.getRequests().get(i);
            if (quotaRequestInput == null) {
                continue;
            }
            ValidatedAllocationDestinationSelectionQuotaRequest.Builder requestBuilder
                    = ValidatedAllocationDestinationSelectionQuotaRequest.builder();
            ErrorCollection.Builder<String, TypedError<String>> requestErrorsBuilder = ErrorCollection.builder();
            QuotaChangeRequest quotaChangeRequest = requestsById.get(quotaRequestInput.getQuotaRequestId());
            if (quotaChangeRequest == null) {
                addFieldValidationError(requestErrorsBuilder, "requests." + i + ".quotaRequestId", locale,
                        "quota.change.request.not.found");
            // Check quota request permissions
            } else if (!ResourceRequestAllocationManager.canUserAllocateQuota(quotaChangeRequest,
                    performerContext.getPerson())) {
                addFieldValidationError(requestErrorsBuilder, "requests." + i + ".quotaRequestId", locale,
                        "only.request.author.or.manager.can.allocate.quota");
            } else if (!quotaChangeRequest.getType().equals(QuotaChangeRequest.Type.RESOURCE_PREORDER)) {
                addFieldValidationError(requestErrorsBuilder, "requests." + i + ".quotaRequestId", locale,
                        "invalid.request.type");
            } else {
                requestBuilder.request(quotaChangeRequest);
            }
            for (int j = 0; j < quotaRequestInput.getProviderKeys().size(); j++) {
                String providerKey = quotaRequestInput.getProviderKeys().get(j);
                if (providerKey == null) {
                    continue;
                }
                Service provider = providersByKey.get(providerKey);
                if (provider == null) {
                    addFieldValidationError(requestErrorsBuilder, "requests." + i + ".providersKeys." + j, locale,
                            "resource.provider.not.found");
                } else {
                    requestBuilder.addProvider(provider);
                }
            }
            if (requestErrorsBuilder.hasAnyErrors()) {
                errorsBuilder.add(requestErrorsBuilder);
            } else {
                builder.addRequest(requestBuilder.build());
            }
        }
        if (errorsBuilder.hasAnyErrors()) {
            return Result.failure(errorsBuilder.build());
        }
        return Result.success(builder.build());
    }

    private List<QuotaChangeRequestsDeliveryMappings> mapDestinations(
            ValidatedAllocationDestinationSelectionRequest request) {
        // Skip non-confirmed requests and requests in non-exportable services
        List<ValidatedAllocationDestinationSelectionQuotaRequest> eligibleRequests = new ArrayList<>();
        request.getRequests().forEach(validatedRequest -> {
            if (validatedRequest.getRequest().getStatus() != QuotaChangeRequest.Status.CONFIRMED
                    || isProjectInTrash(validatedRequest.getRequest().getProject())) {
                return;
            }
            eligibleRequests.add(validatedRequest);
        });
        Map<String, List<ValidatedAllocationDestinationSelectionQuotaRequest>> requestsByMapper = new HashMap<>();
        Map<String, Set<Service>> providersByMapper = new HashMap<>();
        Map<String, ResourcesModelMapper> mappersByKey = new HashMap<>();
        eligibleRequests.forEach(validatedRequest -> {
            // Take all request providers or only those that are specified in the filter for this request
            Set<Service> filteredProviders = getRequestProviders(validatedRequest.getRequest(),
                    new HashSet<>(validatedRequest.getProviders()));
            // Take only providers with enabled delivery
            Set<Service> eligibleProviders = filteredProviders.stream().filter(this::isProviderEligible)
                    .collect(Collectors.toSet());
            // Take only providers where there are some not yet allocated resources present
            Set<Service> availableProviders = new HashSet<>();
            validatedRequest.getRequest().getChanges().forEach(change -> {
                if (eligibleProviders.contains(change.getResource().getService())
                        && change.getAmountReady() > change.getAmountAllocating()) {
                    availableProviders.add(change.getResource().getService());
                }
            });
            // Group providers by resource mapper key
            Map<String, Set<Service>> providersByMapperKey = availableProviders.stream().collect(Collectors
                    .groupingBy(p -> p.getSettings().getResourcesMappingBeanName(), Collectors.toSet()));
            // Group providers and requests by actually existing mapper keys, resolve mappers
            providersByMapperKey.forEach((mapperKey, providers) -> {
                ResourcesModelMapper cachedMapper = mappersByKey.get(mapperKey);
                if (cachedMapper != null) {
                    // Mapper already resolved
                    providersByMapper.computeIfAbsent(mapperKey, k -> new HashSet<>()).addAll(providers);
                    requestsByMapper.computeIfAbsent(mapperKey, k -> new ArrayList<>()).add(validatedRequest);
                } else {
                    // Resolve mapper
                    Optional<ResourcesModelMapper> mapperO = resourcesModelMapperManager
                            .getMapperForProvider(providers.iterator().next());
                    if (mapperO.isPresent()) {
                        mappersByKey.put(mapperKey, mapperO.get());
                        providersByMapper.computeIfAbsent(mapperKey, k -> new HashSet<>()).addAll(providers);
                        requestsByMapper.computeIfAbsent(mapperKey, k -> new ArrayList<>()).add(validatedRequest);
                    }
                }

            });
        });
        // Apply mappers to requests
        List<QuotaChangeRequestsDeliveryMappings> result = new ArrayList<>();
        requestsByMapper.forEach((mapperKey, requests) -> {
            // Take mapper by its key
            ResourcesModelMapper mapper = mappersByKey.get(mapperKey);
            if (mapper == null) {
                return;
            }
            // Take providers supported by mapper
            Set<Service> mapperProviders = providersByMapper.getOrDefault(mapperKey, Set.of());
            if (mapperProviders.isEmpty()) {
                return;
            }
            // For each request where mapper is applicable take either all supported providers
            // or filter them by request providers filter
            Map<Long, Set<Service>> eligibleProvidersPerRequest = new HashMap<>();
            requests.forEach(r -> {
                if (r.getProviders().isEmpty()) {
                    eligibleProvidersPerRequest.put(r.getRequest().getId(), mapperProviders);
                } else {
                    eligibleProvidersPerRequest.put(r.getRequest().getId(),
                            Sets.intersection(new HashSet<>(r.getProviders()), mapperProviders));
                }
            });
            // Apply mapper to requests
            List<QuotaChangeRequest> requestsToMap = requests.stream()
                    .map(ValidatedAllocationDestinationSelectionQuotaRequest::getRequest).collect(Collectors.toList());
            result.add(mapper.mapToDestinations(requestsToMap, mapperProviders, eligibleProvidersPerRequest));
        });
        return result;
    }

    private DeliveryDestinationResponseDto convertDestinations(
            Map<Long, QuotaChangeRequestDeliveryMapping> mergedDestinations, Person person, Locale locale) {
        // Prepare request for D API
        List<DeliveryDestinationDto> deliverables = new ArrayList<>();
        mergedDestinations.forEach((requestId, requestDestinations) -> {
            // Collect all external resource ids for the request
            Set<String> resourceIds = new HashSet<>();
            requestDestinations.getDeliveryMapping().values().forEach(destinations -> destinations
                    .forEach(destination -> resourceIds.add(destination.getResourceId().toString())));
            if (!resourceIds.isEmpty()) {
                // Empty resource ids list won't be accepted
                deliverables.add(new DeliveryDestinationDto(Objects.requireNonNull(requestDestinations
                        .getQuotaChangeRequest().getProject().getAbcServiceId()).longValue(), requestId,
                        new ArrayList<>(resourceIds)));
            }
        });
        if (deliverables.isEmpty()) {
            // Empty request won't be accepted so short circuit right there
            return new DeliveryDestinationResponseDto(List.of(), List.of());
        }
        // Call D API to get accounts for resources
        DeliveryDestinationRequestDto requestDto = new DeliveryDestinationRequestDto(String.valueOf(person.getUid()),
                deliverables);
        return dApiHelper.findDestinations(toLang(locale), requestDto);
    }

    private String toLang(Locale locale) {
        if (Objects.equals(locale, SessionInitializer.RU)) {
            return "ru_RU";
        } else {
            return "en_US";
        }
    }

    private Map<Long, QuotaChangeRequestDeliveryMapping> mergeDestinations(
            List<QuotaChangeRequestsDeliveryMappings> destinations) {
        // Need to merge mapped resources form different mappers
        Map<Long, QuotaChangeRequestDeliveryMapping> result = new HashMap<>();
        Map<Long, QuotaChangeRequest> requestsById = new HashMap<>();
        Map<Long, Map<DeliverableResource, Set<ExternalDeliverableResource>>> destinationsByRequestId
                = new HashMap<>();
        // Group and merge
        destinations.forEach(destination -> {
            destination.getMappingsByRequestId().forEach((requestId, requestDestinations) -> {
                requestsById.put(requestId, requestDestinations.getQuotaChangeRequest());
                requestDestinations.getDeliveryMapping().forEach((source, sourceDestinations) -> {
                    destinationsByRequestId.computeIfAbsent(requestId, k -> new HashMap<>()).computeIfAbsent(source,
                            z -> new HashSet<>()).addAll(sourceDestinations);
                });
            });
        });
        // Add full request object to merged result
        requestsById.forEach((requestId, request) -> {
            result.put(requestId, new QuotaChangeRequestDeliveryMapping(destinationsByRequestId
                    .getOrDefault(requestId, Map.of()), request));
        });
        return result;
    }

    private AllocationDestinationSelectionResponse doFind(
            ValidatedAllocationDestinationSelectionRequest request, Locale locale) {
        // Map internal resources to external resources
        List<QuotaChangeRequestsDeliveryMappings> mappedDestinations = mapDestinations(request);
        // Merge mapped resources from different mappers
        Map<Long, QuotaChangeRequestDeliveryMapping> mergedDestinations = mergeDestinations(mappedDestinations);
        // Get accounts for external resources from D API
        DeliveryDestinationResponseDto convertedDestinations = convertDestinations(mergedDestinations,
                request.getAuthor(), locale);
        // Group D API responses by request id
        Map<Long, DeliveryDestinationProvidersDto> convertedDestinationsByRequest = convertedDestinations
                .getDestinations().stream().collect(Collectors.toMap(DeliveryDestinationProvidersDto::getQuotaRequestId,
                        Function.identity()));
        // Group external resource details by external resource id
        Map<String, DeliveryDestinationResourceDto> convertedResourceById = convertedDestinations.getResources()
                .stream().collect(Collectors.toMap(DeliveryDestinationResourceDto::getId, Function.identity()));
        // Result builder
        AllocationDestinationSelectionResponse.Builder builder = AllocationDestinationSelectionResponse.builder();
        // Process all requests
        request.getRequests().forEach(validatedRequest -> {
            // Non-confirmed requests are skipped with corresponding message
            if (validatedRequest.getRequest().getStatus() != QuotaChangeRequest.Status.CONFIRMED) {
                builder.addRequest(AllocationDestinationQuotaRequest.builder()
                        .quotaRequestId(validatedRequest.getRequest().getId())
                        .eligible(false)
                        .addIneligibilityReason(getErrorMessage(locale, "only.confirmed.request.can.be.allocated"))
                        .build());
                return;
            }
            // Requests for non-exportable services are skipped with corresponding message
            if (isProjectInTrash(validatedRequest.getRequest().getProject())) {
                builder.addRequest(AllocationDestinationQuotaRequest.builder()
                        .quotaRequestId(validatedRequest.getRequest().getId())
                        .eligible(false)
                        .addIneligibilityReason(getErrorMessage(locale, "request.cannot.be.allocated.in.wrong.project"))
                        .build());
                return;
            }
            // Mapped resources for this request
            QuotaChangeRequestDeliveryMapping mappedRequest = mergedDestinations
                    .get(validatedRequest.getRequest().getId());
            // Accounts for this request
            DeliveryDestinationProvidersDto convertedRequest = convertedDestinationsByRequest
                    .get(validatedRequest.getRequest().getId());
            boolean eligibleRequest = true;
            // Result builder for this request
            AllocationDestinationQuotaRequest.Builder requestBuilder = AllocationDestinationQuotaRequest.builder();
            requestBuilder.quotaRequestId(validatedRequest.getRequest().getId());
            if (convertedRequest != null && !convertedRequest.isEligible()) {
                // D API may deem this request ineligible for delivery, take this into account
                eligibleRequest = false;
                convertedRequest.getIneligibilityReasons().forEach(requestBuilder::addIneligibilityReason);
            }
            requestBuilder.eligible(eligibleRequest);
            // Either take all request providers or only providers specified in the request
            HashSet<Service> providersFilter = new HashSet<>(validatedRequest.getProviders());
            Set<Service> providers = getRequestProviders(validatedRequest.getRequest(), providersFilter);
            // Take only allocatable changes, group by provider
            Map<Service, Set<QuotaChangeRequest.Change>> allocatableChangesByProvider
                    = groupAllocatableChangesByProvider(validatedRequest.getRequest());
            // Mapped resources by provider
            Map<Service, Map<DeliverableResource, Set<ExternalDeliverableResource>>> mappingByProvider
                    = new HashMap<>();
            // Providers for external resource id
            Map<String, Set<Service>> providersForExternalResourceId = new HashMap<>();
            // Internal resources for external resource id
            Map<String, Set<DeliverableResource>> sourcesForExternalResourceId = new HashMap<>();
            if (mappedRequest != null) {
                mappedRequest.getDeliveryMapping().forEach((source, destinations) -> {
                    // Group mapped resources by provider
                    mappingByProvider.computeIfAbsent(source.getResource().getService(), k -> new HashMap<>())
                            .put(source, destinations);
                    destinations.forEach(destination -> {
                        // Group providers by external resource id
                        providersForExternalResourceId.computeIfAbsent(destination
                            .getResourceId().toString(), k -> new HashSet<>()).add(source.getResource().getService());
                        // Group internal resources by external resource id
                        sourcesForExternalResourceId.computeIfAbsent(destination.getResourceId().toString(),
                                k -> new HashSet<>()).add(source);
                    });
                });
            }
            // Accounts by provider
            Map<Service, List<DeliveryProviderDto>> convertedResourcesByProvider = new HashMap<>();
            if (convertedRequest != null) {
                // For each external provider
                convertedRequest.getProviders().forEach(p -> {
                    // Collect providers having matching external resources
                    Set<Service> matchingProviders = new HashSet<>();
                    // For each external accounts space
                    p.getAccountsSpaces().forEach(s -> {
                        s.getResourcesAccounts().getResourceIds().forEach(r -> matchingProviders
                                .addAll(providersForExternalResourceId.getOrDefault(r, Set.of())));
                    });
                    // If no accounts spaces in external provider
                    p.getResourcesAccounts().ifPresent(ra -> ra.getResourceIds().forEach(r -> matchingProviders
                            .addAll(providersForExternalResourceId.getOrDefault(r, Set.of()))));
                    // For each matching provider remember accounts
                    matchingProviders.forEach(s -> convertedResourcesByProvider
                            .computeIfAbsent(s, k -> new ArrayList<>()).add(p));
                });
            }
            // Build result for each provider
            providers.forEach(provider -> processProvider(provider, requestBuilder, providersFilter,
                    convertedResourceById, mappedRequest, convertedRequest, allocatableChangesByProvider,
                    mappingByProvider, sourcesForExternalResourceId, convertedResourcesByProvider, locale));
            builder.addRequest(requestBuilder.build());
        });
        return builder.build();
    }

    private void processProvider(
            Service provider,
            AllocationDestinationQuotaRequest.Builder requestBuilder,
            HashSet<Service> providersFilter,
            Map<String, DeliveryDestinationResourceDto> convertedResourceById,
            QuotaChangeRequestDeliveryMapping mappedRequest,
            DeliveryDestinationProvidersDto convertedRequest,
            Map<Service, Set<QuotaChangeRequest.Change>> allocatableChangesByProvider,
            Map<Service, Map<DeliverableResource, Set<ExternalDeliverableResource>>> mappingByProvider,
            Map<String, Set<DeliverableResource>> sourcesForExternalResourceId,
            Map<Service, List<DeliveryProviderDto>> convertedResourcesByProvider,
            Locale locale) {
        // Skip provider if it's not in the filter or there is no filter and there is nothing for this provider
        if (skipProvider(allocatableChangesByProvider, provider, providersFilter)) {
            return;
        }
        // Result builder for the provider
        AllocationDestinationProvider.Builder providerBuilder = AllocationDestinationProvider.builder();
        providerBuilder.providerKey(provider.getKey());
        providerBuilder.providerName(provider.getName());
        providerBuilder.eligible(true);
        // Mapper for provider, to skip providers without mappers
        ResourcesModelMapper mapperForProvider = resourcesModelMapperManager
                .getMapperForProvider(provider).orElse(null);
        Set<String> providerIneligibilityReasons = new HashSet<>();
        // Skip non-mappable providers
        if (isProviderNonEligible(provider, mapperForProvider, providerIneligibilityReasons, locale)) {
            providerBuilder.eligible(false);
            providerIneligibilityReasons.forEach(providerBuilder::addIneligibilityReason);
            requestBuilder.addProvider(providerBuilder.build());
            return;
        }
        // Allocatable changes
        Set<QuotaChangeRequest.Change> allocatableChanges = allocatableChangesByProvider
                .getOrDefault(provider, Set.of());
        // Skip if no allocatable changes
        if (mappedRequest == null || convertedRequest == null || allocatableChanges.isEmpty()) {
            requestBuilder.addProvider(providerBuilder.build());
            return;
        }
        // Mapped resources for this provider
        Map<DeliverableResource, Set<ExternalDeliverableResource>> mapping = mappingByProvider
                .getOrDefault(provider, Map.of());
        // Accounts for this provider
        List<DeliveryProviderDto> externalProviders = convertedResourcesByProvider
                .getOrDefault(provider, List.of());
        // For each external provider
        externalProviders.forEach(p -> {
            if (p.getResourcesAccounts().isPresent()) {
                // No accounts spaces
                processProviderNoAccountsSpaces(providerBuilder, p, convertedResourceById,
                        sourcesForExternalResourceId, mapping);
            } else {
                // For each accounts space
                p.getAccountsSpaces().forEach(accountsSpace -> processAccountsSpace(providerBuilder, p, accountsSpace,
                        convertedResourceById, sourcesForExternalResourceId, mapping));
            }
        });
        requestBuilder.addProvider(providerBuilder.build());
    }

    private void processAccountsSpace(
            AllocationDestinationProvider.Builder providerBuilder,
            DeliveryProviderDto deliveryProvider,
            DeliveryAccountsSpaceDto accountsSpace,
            Map<String, DeliveryDestinationResourceDto> convertedResourceById,
            Map<String, Set<DeliverableResource>> sourcesForExternalResourceId,
            Map<DeliverableResource, Set<ExternalDeliverableResource>> mapping) {
        DeliveryResourcesAccountsDto resourcesAccounts = accountsSpace.getResourcesAccounts();
        List<AllocationDestinationSourceResource> resources = new ArrayList<>();
        List<AllocationDestinationTargetAccount> targetAccounts = new ArrayList<>();
        // For each account, add it to result
        resourcesAccounts.getAccounts().forEach(account -> {
            targetAccounts.add(new AllocationDestinationTargetAccount(account.getId(),
                    accountName(account), account.getFolderId(), account.getFolderName(), deliveryProvider.getId(),
                    deliveryProvider.getName(), accountsSpace.getId(), accountsSpace.getName()));
        });
        // Collect internal resources matching external resources
        Set<DeliverableResource> matchingSourceResources = new HashSet<>();
        resourcesAccounts.getResourceIds().forEach(resourceId -> matchingSourceResources
                .addAll(sourcesForExternalResourceId.getOrDefault(resourceId, Set.of())));
        // For each collected internal resource
        matchingSourceResources.forEach(sourceResource -> {
            // Matching external resources
            List<AllocationDestinationTargetResource> targetResources = new ArrayList<>();
            // For each matching external resource
            mapping.getOrDefault(sourceResource, Set.of()).forEach(destinationResource -> {
                // Matching external resource
                DeliveryDestinationResourceDto externalResource = convertedResourceById
                        .get(destinationResource.getResourceId().toString());
                if (externalResource != null) {
                    // Collect ineligibility reasons
                    boolean eligible = externalResource.getEligible() && accountsSpace.isEligible()
                            && deliveryProvider.isEligible();
                    Set<String> ineligibilityReasons = new HashSet<>();
                    ineligibilityReasons.addAll(externalResource.getIneligibilityReasons());
                    ineligibilityReasons.addAll(deliveryProvider.getIneligibilityReasons());
                    ineligibilityReasons.addAll(accountsSpace.getIneligibilityReasons());
                    targetResources.add(new AllocationDestinationTargetResource(
                            externalResource.getId(), externalResource.getName(), eligible,
                            new ArrayList<>(ineligibilityReasons)));
                }
            });
            // Internal resource segments
            List<AllocationDestinationSourceResourceSegment> segments = sourceResource
                    .getSegments().stream().map(s -> new AllocationDestinationSourceResourceSegment(
                            s.getSegmentation().getKey().getPublicKey(),
                            s.getSegmentation().getName(),
                            s.getPublicKey(), s.getName())).collect(Collectors.toList());
            resources.add(new AllocationDestinationSourceResource(sourceResource.getResource()
                    .getPublicKey(), sourceResource.getResource().getName(), segments,
                    targetResources));
        });
        // Add resulting destination group
        AllocationDestinationGroup destinationGroup = new AllocationDestinationGroup(
                deliveryProvider.getName() + " - " + accountsSpace.getName(), resources, targetAccounts);
        providerBuilder.addDestinationGroup(destinationGroup);
    }

    private void processProviderNoAccountsSpaces(
            AllocationDestinationProvider.Builder providerBuilder,
            DeliveryProviderDto deliveryProvider,
            Map<String, DeliveryDestinationResourceDto> convertedResourceById,
            Map<String, Set<DeliverableResource>> sourcesForExternalResourceId,
            Map<DeliverableResource, Set<ExternalDeliverableResource>> mapping) {
        DeliveryResourcesAccountsDto resourcesAccounts = deliveryProvider.getResourcesAccounts().orElseThrow();
        List<AllocationDestinationSourceResource> resources = new ArrayList<>();
        List<AllocationDestinationTargetAccount> targetAccounts = new ArrayList<>();
        // For each account, add it to result
        resourcesAccounts.getAccounts().forEach(account -> {
            targetAccounts.add(new AllocationDestinationTargetAccount(account.getId(),
                    accountName(account), account.getFolderId(), account.getFolderName(), deliveryProvider.getId(),
                    deliveryProvider.getName(), null, null));
        });
        // Collect internal resources matching external resources
        Set<DeliverableResource> matchingSourceResources = new HashSet<>();
        resourcesAccounts.getResourceIds().forEach(resourceId -> matchingSourceResources
                .addAll(sourcesForExternalResourceId.getOrDefault(resourceId, Set.of())));
        // For each collected internal resource
        matchingSourceResources.forEach(sourceResource -> {
            // Matching external resources
            List<AllocationDestinationTargetResource> targetResources = new ArrayList<>();
            // For each matching external resource
            mapping.getOrDefault(sourceResource, Set.of()).forEach(destinationResource -> {
                DeliveryDestinationResourceDto externalResource = convertedResourceById
                        .get(destinationResource.getResourceId().toString());
                if (externalResource != null) {
                    // Collect ineligibility reasons
                    boolean eligible = externalResource.getEligible() && deliveryProvider.isEligible();
                    Set<String> ineligibilityReasons = new HashSet<>();
                    ineligibilityReasons.addAll(externalResource.getIneligibilityReasons());
                    ineligibilityReasons.addAll(deliveryProvider.getIneligibilityReasons());
                    targetResources.add(new AllocationDestinationTargetResource(
                            externalResource.getId(), externalResource.getName(), eligible,
                            new ArrayList<>(ineligibilityReasons)));
                }
            });
            // Internal resource segments
            List<AllocationDestinationSourceResourceSegment> segments = sourceResource
                    .getSegments().stream().map(s -> new AllocationDestinationSourceResourceSegment(
                            s.getSegmentation().getKey().getPublicKey(),
                            s.getSegmentation().getName(),
                            s.getPublicKey(), s.getName())).collect(Collectors.toList());
            resources.add(new AllocationDestinationSourceResource(sourceResource.getResource()
                    .getPublicKey(), sourceResource.getResource().getName(), segments,
                    targetResources));
        });
        // Add resulting destination group
        AllocationDestinationGroup destinationGroup = new AllocationDestinationGroup(deliveryProvider.getName(),
                resources, targetAccounts);
        providerBuilder.addDestinationGroup(destinationGroup);
    }

    private String accountName(DeliveryAccountDto account) {
        return account.getDisplayName().orElseGet(() -> account.getExternalKey().orElseGet(account::getExternalId));
    }

    private Set<Service> getRequestProviders(QuotaChangeRequest request, Set<Service> providersFilter) {
        if (providersFilter.isEmpty()) {
            return request.getChanges().stream().map(change -> change.getResource().getService())
                    .collect(Collectors.toSet());
        }
        return providersFilter;
    }

    private Map<Service, Set<QuotaChangeRequest.Change>> groupAllocatableChangesByProvider(QuotaChangeRequest request) {
        return request.getChanges().stream().filter(c -> c.getAmountReady() > c.getAmountAllocating())
                .collect(Collectors.groupingBy(c -> c.getResource().getService(), Collectors.toSet()));
    }

    private boolean skipProvider(Map<Service, Set<QuotaChangeRequest.Change>> allocatableChangesByProvider,
                                 Service provider, Set<Service> providersFilter) {
        if (providersFilter.isEmpty()) {
            return allocatableChangesByProvider.getOrDefault(provider, Set.of()).isEmpty();
        }
        return !providersFilter.contains(provider);
    }

    private boolean isProjectInTrash(Project project) {
        return project.getPathToRoot().stream().anyMatch(p -> Project.TRASH_PROJECT_KEY.equals(p.getPublicKey()));
    }

    private boolean isProviderEligible(Service provider) {
        return provider.getSettings().isManualQuotaAllocation()
                && provider.getSettings().getResourcesMappingBeanName() != null;
    }

    private boolean isProviderNonEligible(Service provider, ResourcesModelMapper mapper,
                                          Set<String> reasons, Locale locale) {
        if (!provider.getSettings().isManualQuotaAllocation()) {
            reasons.add(getErrorMessage(locale, "manual.allocation.unavailable.for", provider.getName()));
            return true;
        }
        if (provider.getSettings().getResourcesMappingBeanName() == null || mapper == null) {
            reasons.add(getErrorMessage(locale, "no.allocation.bean.for.resources.model.provider",
                    provider.getName()));
            return true;
        }
        return false;
    }

    private void addFieldValidationError(ErrorCollection.Builder<String, TypedError<String>> errorsBuilder,
                                         String fieldKey, Locale locale, String messageKey, Object... args) {
        errorsBuilder.addError(fieldKey, TypedError.invalid(LocalizationUtils
                .resolveWithDefaultAsKey(errorMessageSource, LocalizableString.of(messageKey, args), locale)));
    }

    private String getErrorMessage(Locale locale, String messageKey, Object... args) {
        return LocalizationUtils.resolveWithDefaultAsKey(errorMessageSource, LocalizableString.of(messageKey, args),
                locale);
    }

}
