package com.commercetools.sync.products.helpers;

import com.commercetools.sync.commons.exceptions.ReferenceResolutionException;
import com.commercetools.sync.commons.helpers.AssetReferenceResolver;
import com.commercetools.sync.commons.helpers.BaseReferenceResolver;
import com.commercetools.sync.products.ProductSyncOptions;
import com.commercetools.sync.services.CategoryService;
import com.commercetools.sync.services.ChannelService;
import com.commercetools.sync.services.CustomerGroupService;
import com.commercetools.sync.services.ProductService;
import com.commercetools.sync.services.ProductTypeService;
import com.commercetools.sync.services.TypeService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.sphere.sdk.categories.Category;
import io.sphere.sdk.models.AssetDraft;
import io.sphere.sdk.products.PriceDraft;
import io.sphere.sdk.products.Product;
import io.sphere.sdk.products.ProductVariantDraft;
import io.sphere.sdk.products.ProductVariantDraftBuilder;
import io.sphere.sdk.products.attributes.AttributeDraft;
import io.sphere.sdk.producttypes.ProductType;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

import static com.commercetools.sync.commons.utils.CompletableFutureUtils.mapValuesToFutureOfCompletedValues;
import static com.commercetools.sync.commons.utils.ResourceIdentifierUtils.REFERENCE_ID_FIELD;
import static com.commercetools.sync.commons.utils.ResourceIdentifierUtils.REFERENCE_TYPE_ID_FIELD;
import static com.commercetools.sync.commons.utils.ResourceIdentifierUtils.isReferenceOfType;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.stream.Collectors.toList;


public final class VariantReferenceResolver extends BaseReferenceResolver<ProductVariantDraft, ProductSyncOptions> {
    private final PriceReferenceResolver priceReferenceResolver;
    private final AssetReferenceResolver assetReferenceResolver;
    private final ProductService productService;
    private final ProductTypeService productTypeService;
    private final CategoryService categoryService;

    /**
     * Instantiates a {@link VariantReferenceResolver} instance that could be used to resolve the variants of product
     * drafts in the CTP project specified in the injected {@link ProductSyncOptions} instance.
     *
     * @param productSyncOptions   the container of all the options of the sync process including the CTP project client
     *                             and/or configuration and other sync-specific options.
     * @param typeService          the service to fetch the custom types for reference resolution.
     * @param channelService       the service to fetch the channels for reference resolution.
     * @param customerGroupService the service to fetch the customer groups for reference resolution.
     * @param productService       the service to fetch the products for reference resolution.
     * @param productTypeService   the service to fetch the productTypes for reference resolution.
     * @param categoryService      the service to fetch the categories for reference resolution.
     */
    public VariantReferenceResolver(@Nonnull final ProductSyncOptions productSyncOptions,
                                    @Nonnull final TypeService typeService,
                                    @Nonnull final ChannelService channelService,
                                    @Nonnull final CustomerGroupService customerGroupService,
                                    @Nonnull final ProductService productService,
                                    @Nonnull final ProductTypeService productTypeService,
                                    @Nonnull final CategoryService categoryService) {
        super(productSyncOptions);
        this.priceReferenceResolver = new PriceReferenceResolver(productSyncOptions, typeService, channelService,
            customerGroupService);
        this.assetReferenceResolver = new AssetReferenceResolver(productSyncOptions, typeService);
        this.productService = productService;
        this.categoryService = categoryService;
        this.productTypeService = productTypeService;
    }


    /**
     * Given a {@link ProductVariantDraft} this method attempts to resolve the prices, assets and attributes to
     * return a {@link CompletionStage} which contains a new instance of the draft with the resolved
     * references. The keys of the references are either taken from the expanded references or
     * taken from the id field of the references.
     *
     * <p>Note: this method will filter out any null sub resources (e.g. prices, attributes or assets) under the
     * returned resolved variant.
     *
     * @param productVariantDraft the product variant draft to resolve it's references.
     * @return a {@link CompletionStage} that contains as a result a new productDraft instance with resolved references
     *         or, in case an error occurs during reference resolution, a {@link ReferenceResolutionException}.
     */
    @Override
    public CompletionStage<ProductVariantDraft> resolveReferences(
        @Nonnull final ProductVariantDraft productVariantDraft) {
        return resolvePricesReferences(ProductVariantDraftBuilder.of(productVariantDraft))
            .thenCompose(this::resolveAssetsReferences)
            .thenCompose(this::resolveAttributesReferences)
            .thenApply(ProductVariantDraftBuilder::build);
    }

    @Nonnull
    CompletionStage<ProductVariantDraftBuilder> resolveAssetsReferences(
        @Nonnull final ProductVariantDraftBuilder productVariantDraftBuilder) {

        final List<AssetDraft> productVariantDraftAssets = productVariantDraftBuilder.getAssets();
        if (productVariantDraftAssets == null) {
            return completedFuture(productVariantDraftBuilder);
        }

        return mapValuesToFutureOfCompletedValues(productVariantDraftAssets,
            assetReferenceResolver::resolveReferences, toList()).thenApply(productVariantDraftBuilder::assets);
    }

    @Nonnull
    CompletionStage<ProductVariantDraftBuilder> resolvePricesReferences(
        @Nonnull final ProductVariantDraftBuilder productVariantDraftBuilder) {

        final List<PriceDraft> productVariantDraftPrices = productVariantDraftBuilder.getPrices();
        if (productVariantDraftPrices == null) {
            return completedFuture(productVariantDraftBuilder);
        }

        return mapValuesToFutureOfCompletedValues(productVariantDraftPrices,
            priceReferenceResolver::resolveReferences, toList())
            .thenApply(productVariantDraftBuilder::prices);
    }

    @Nonnull
    private CompletionStage<ProductVariantDraftBuilder> resolveAttributesReferences(
        @Nonnull final ProductVariantDraftBuilder productVariantDraftBuilder) {

        final List<AttributeDraft> attributeDrafts = productVariantDraftBuilder.getAttributes();
        if (attributeDrafts == null) {
            return completedFuture(productVariantDraftBuilder);
        }

        return mapValuesToFutureOfCompletedValues(attributeDrafts, this::resolveAttributeReference, toList())
                                     .thenApply(productVariantDraftBuilder::attributes);
    }

    @Nonnull
    private CompletionStage<AttributeDraft> resolveAttributeReference(@Nonnull final AttributeDraft attributeDraft) {

        final JsonNode attributeDraftValue = attributeDraft.getValue();

        if (attributeDraftValue == null) {
            return CompletableFuture.completedFuture(attributeDraft);
        }

        final JsonNode attributeDraftValueClone = attributeDraftValue.deepCopy();

        final List<JsonNode> allAttributeReferences = attributeDraftValueClone.findParents(REFERENCE_TYPE_ID_FIELD);

        if (!allAttributeReferences.isEmpty()) {
            return mapValuesToFutureOfCompletedValues(allAttributeReferences, this::resolveReference, toList())
                .thenApply(ignoredResult -> AttributeDraft.of(attributeDraft.getName(), attributeDraftValueClone));
        }

        return CompletableFuture.completedFuture(attributeDraft);
    }

    @Nonnull
    private CompletionStage<Void> resolveReference(@Nonnull final JsonNode referenceValue) {
        return getResolvedId(referenceValue)
            .thenAccept(optionalId ->
                optionalId.ifPresent(id -> ((ObjectNode) referenceValue).put(REFERENCE_ID_FIELD, id)));
    }

    @Nonnull
    private CompletionStage<Optional<String>> getResolvedId(@Nonnull final JsonNode referenceValue) {

        if (isReferenceOfType(referenceValue, Product.referenceTypeId())) {
            return getResolvedIdFromKeyInReference(referenceValue, productService::getIdFromCacheOrFetch);
        }

        if (isReferenceOfType(referenceValue, Category.referenceTypeId())) {
            return getResolvedIdFromKeyInReference(referenceValue, categoryService::fetchCachedCategoryId);
        }

        if (isReferenceOfType(referenceValue, ProductType.referenceTypeId())) {
            return getResolvedIdFromKeyInReference(referenceValue, productTypeService::fetchCachedProductTypeId);
        }

        return CompletableFuture.completedFuture(Optional.empty());
    }

    @Nonnull
    private CompletionStage<Optional<String>> getResolvedIdFromKeyInReference(
        @Nonnull final JsonNode referenceValue,
        @Nonnull final Function<String, CompletionStage<Optional<String>>> resolvedIdFetcher) {

        final JsonNode idField = referenceValue.get(REFERENCE_ID_FIELD);
        return idField != null && !Objects.equals(idField, NullNode.getInstance())
            ? resolvedIdFetcher.apply(idField.asText())
            : CompletableFuture.completedFuture(Optional.empty());
    }
}