package games.moisoni.google_iab;

import static com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.DEVELOPER_ERROR;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_NOT_OWNED;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.NETWORK_ERROR;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.OK;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_DISCONNECTED;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED;
import static com.android.billingclient.api.BillingClient.FeatureType.SUBSCRIPTIONS;
import static com.android.billingclient.api.BillingClient.ProductType.INAPP;
import static com.android.billingclient.api.BillingClient.ProductType.SUBS;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;

import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.PendingPurchasesParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryPurchasesParams;
import com.google.common.collect.ImmutableList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import games.moisoni.google_iab.enums.ErrorType;
import games.moisoni.google_iab.enums.ProductType;
import games.moisoni.google_iab.enums.PurchasedResult;
import games.moisoni.google_iab.enums.SkuProductType;
import games.moisoni.google_iab.enums.SupportState;
import games.moisoni.google_iab.listeners.AcknowledgeEventListener;
import games.moisoni.google_iab.listeners.BillingEventListener;
import games.moisoni.google_iab.listeners.ConsumeEventListener;
import games.moisoni.google_iab.models.BillingResponse;
import games.moisoni.google_iab.models.ProductInfo;
import games.moisoni.google_iab.models.PurchaseInfo;

public class BillingConnector implements DefaultLifecycleObserver {

    private final Handler uiHandler;

    private static final String TAG = "BillingConnector";
    private static final int defaultResponseCode = 99; //custom response code not used by the official BillingClient API

    private static final int notAnOffer = -1;

    private static final long RECONNECT_TIMER_START_MILLISECONDS = 1000L;
    private static final long RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L;
    private final AtomicLong reconnectMilliseconds = new AtomicLong(RECONNECT_TIMER_START_MILLISECONDS);

    private static final int MAX_PENDING_RETRIES = 3;
    private static final long INITIAL_RETRY_DELAY_MS = 1000L;
    private static final long MAX_RETRY_DELAY_MS = 10000L;
    private static final long MAX_PENDING_DURATION_MS = 1000 * 60 * 5;

    private final String base64Key;

    private final Context context;
    private Lifecycle lifecycle;

    private BillingClient billingClient;
    private BillingEventListener billingEventListener;

    private List<String> consumableIds;
    private List<String> nonConsumableIds;
    private List<String> subscriptionIds;

    private final List<QueryProductDetailsParams.Product> allProductList = new ArrayList<>();

    private final List<ProductInfo> fetchedProductInfoList = new ArrayList<>();
    private final List<PurchaseInfo> purchasedProductsList = new ArrayList<>();

    private final Object purchasedProductsSync = new Object(); //object for thread safety

    private int productDetailsQueriesPending;
    private int purchaseQueriesPending;

    private boolean shouldAutoAcknowledge = false;
    private boolean shouldAutoConsume = false;
    private boolean shouldEnableLogging = false;

    private volatile boolean isConnected = false;
    private volatile boolean fetchedPurchasedProducts = false;

    /**
     * BillingConnector public constructor
     *
     * @param context   - is the application context
     * @param base64Key - is the public developer key from Play Console
     * @param lifecycle - (optional) the lifecycle object to automatically manage the BillingConnector's
     *                  lifecycle. If provided, the connector will automatically handle connection
     *                  cleanup when the lifecycle owner is destroyed. Can be null if manual lifecycle
     *                  management is preferred.
     */
    public BillingConnector(@NonNull Context context, String base64Key, @Nullable Lifecycle lifecycle) {
        this.context = context.getApplicationContext();
        this.base64Key = base64Key;
        if (lifecycle != null) {
            this.lifecycle = lifecycle;
            lifecycle.addObserver(this);
        }
        this.uiHandler = new Handler(Looper.getMainLooper());
        this.init();
    }

    /**
     * To initialize BillingConnector
     */
    private void init() {
        billingClient = BillingClient.newBuilder(context)
                .enablePendingPurchases(PendingPurchasesParams.newBuilder().enablePrepaidPlans().enableOneTimeProducts().build())
                .setListener(this::onPurchasesUpdated)
                .build();
    }

    private void onPurchasesUpdated(@NonNull BillingResult billingResult, List<Purchase> purchases) {
        switch (billingResult.getResponseCode()) {
            case OK:
                if (purchases != null) {
                    processPurchases(ProductType.COMBINED, purchases, false);
                }
                break;
            case USER_CANCELED:
                Log("User pressed back or canceled a dialog." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.USER_CANCELED, billingResult)));
                break;
            case SERVICE_UNAVAILABLE:
                Log("Network connection is down." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.SERVICE_UNAVAILABLE, billingResult)));
                break;
            case BILLING_UNAVAILABLE:
                Log("Billing API version is not supported for the type requested." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.BILLING_UNAVAILABLE, billingResult)));
                break;
            case ITEM_UNAVAILABLE:
                Log("Requested product is not available for purchase." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.ITEM_UNAVAILABLE, billingResult)));
                break;
            case DEVELOPER_ERROR:
                Log("Invalid arguments provided to the API." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.DEVELOPER_ERROR, billingResult)));
                break;
            case ERROR:
                Log("Fatal error during the API action." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.ERROR, billingResult)));
                break;
            case ITEM_ALREADY_OWNED:
                Log("Failure to purchase since item is already owned." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.ITEM_ALREADY_OWNED, billingResult)));
                break;
            case ITEM_NOT_OWNED:
                Log("Failure to consume since item is not owned." + " Response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.ITEM_NOT_OWNED, billingResult)));
                break;
            case SERVICE_DISCONNECTED:
                Log("Initialization error: service disconnected/timeout. Trying to reconnect...");
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.CLIENT_DISCONNECTED, billingResult)));
                break;
            case NETWORK_ERROR:
                Log("Initialization error: service network error. Trying to reconnect...");
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.NETWORK_ERROR, billingResult)));
                break;
            default:
                Log("Initialization error: " + new BillingResponse(ErrorType.BILLING_ERROR, billingResult));
                break;
        }
    }

    /**
     * To attach an event listener to establish a bridge with the caller
     */
    public final void setBillingEventListener(BillingEventListener billingEventListener) {
        this.billingEventListener = billingEventListener;
    }

    /**
     * To set consumable products ids
     */
    public final BillingConnector setConsumableIds(List<String> consumableIds) {
        this.consumableIds = consumableIds;
        return this;
    }

    /**
     * To set non-consumable products ids
     */
    public final BillingConnector setNonConsumableIds(List<String> nonConsumableIds) {
        this.nonConsumableIds = nonConsumableIds;
        return this;
    }

    /**
     * To set subscription products ids
     */
    public final BillingConnector setSubscriptionIds(List<String> subscriptionIds) {
        this.subscriptionIds = subscriptionIds;
        return this;
    }

    /**
     * To auto acknowledge the purchase
     */
    public final BillingConnector autoAcknowledge() {
        shouldAutoAcknowledge = true;
        return this;
    }

    /**
     * To auto consume the purchase
     */
    public final BillingConnector autoConsume() {
        shouldAutoConsume = true;
        return this;
    }

    /**
     * To enable logging for debugging
     */
    public final BillingConnector enableLogging() {
        shouldEnableLogging = true;
        return this;
    }

    /**
     * Returns the state of the billing client
     */
    public final boolean isReady() {
        if (!isConnected) {
            Log("Billing client is not ready because no connection is established yet");
        }

        if (!billingClient.isReady()) {
            Log("Billing client is not ready yet");
        }

        return isConnected && billingClient.isReady() && !fetchedProductInfoList.isEmpty();
    }

    /**
     * Returns a boolean state of the product
     *
     * @param productId - is the product id that has to be checked
     */
    private boolean checkProductBeforeInteraction(String productId) {
        if (!isReady()) {
            findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.CLIENT_NOT_READY,
                    "Client is not ready yet", defaultResponseCode)));
            return false;
        }

        boolean productExists = false;
        if (productId != null) {
            for (ProductInfo productInfo : fetchedProductInfoList) {
                if (productInfo.getProduct().equals(productId)) {
                    productExists = true;
                    break;
                }
            }
        }

        if (productId != null && !productExists) {
            findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.PRODUCT_NOT_EXIST,
                    "The product id: " + productId + " doesn't seem to exist on Play Console", defaultResponseCode)));
            return false;
        }
        return true;
    }

    /**
     * To connect the billing client with Play Console
     */
    public final BillingConnector connect() {
        if (!isPlayStoreInstalled(context)) {
            findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.PLAY_STORE_NOT_INSTALLED,
                    "Google Play Store is not installed", BILLING_UNAVAILABLE)));
            return this;
        }

        List<QueryProductDetailsParams.Product> productInAppList = new ArrayList<>();
        List<QueryProductDetailsParams.Product> productSubsList = new ArrayList<>();

        //set empty list to null so we only have to deal with lists that are null or not empty
        if (consumableIds == null || consumableIds.isEmpty()) {
            consumableIds = null;
        } else {
            for (String id : consumableIds) {
                productInAppList.add(QueryProductDetailsParams.Product.newBuilder().setProductId(id).setProductType(INAPP).build());
            }
        }

        if (nonConsumableIds == null || nonConsumableIds.isEmpty()) {
            nonConsumableIds = null;
        } else {
            for (String id : nonConsumableIds) {
                productInAppList.add(QueryProductDetailsParams.Product.newBuilder().setProductId(id).setProductType(INAPP).build());
            }
        }

        if (subscriptionIds == null || subscriptionIds.isEmpty()) {
            subscriptionIds = null;
        } else {
            for (String id : subscriptionIds) {
                productSubsList.add(QueryProductDetailsParams.Product.newBuilder().setProductId(id).setProductType(SUBS).build());
            }
        }

        allProductList.addAll(productInAppList);
        allProductList.addAll(productSubsList);

        int queryCount = 0;
        if (!productInAppList.isEmpty()) queryCount++;
        if (!productSubsList.isEmpty()) queryCount++;
        productDetailsQueriesPending = queryCount;

        //check if any list is provided
        if (allProductList.isEmpty()) {
            throw new IllegalArgumentException("At least one list of consumables, non-consumables or subscriptions is needed");
        }

        //check for duplicates product ids
        int allIdsSize = allProductList.size();
        int allIdsSizeDistinct = new HashSet<>(allProductList).size();
        if (allIdsSize != allIdsSizeDistinct) {
            throw new IllegalArgumentException("The product id must appear only once in a list. Also, it must not be in different lists");
        }

        Log("Billing service: connecting...");
        if (!billingClient.isReady()) {
            billingClient.startConnection(new BillingClientStateListener() {
                @Override
                public void onBillingServiceDisconnected() {
                    isConnected = false;

                    findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.CLIENT_DISCONNECTED,
                            "Billing service: disconnected", defaultResponseCode)));

                    Log("Billing service: Trying to reconnect...");
                    retryBillingClientConnection();
                }

                @Override
                public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                    switch (billingResult.getResponseCode()) {
                        case OK:
                            isConnected = true;
                            Log("Billing service: connected");

                            //query consumable and non-consumable product details
                            if (!productInAppList.isEmpty()) {
                                queryProductDetails(INAPP, productInAppList);
                            }

                            //query subscription product details
                            if (subscriptionIds != null) {
                                queryProductDetails(SUBS, productSubsList);
                            }
                            break;
                        case BILLING_UNAVAILABLE:
                            Log("Billing service: unavailable");
                            retryBillingClientConnection();
                            break;
                        default:
                            Log("Billing service: error");
                            retryBillingClientConnection();
                            break;
                    }
                }
            });
        }

        return this;
    }

    /**
     * Retries the billing client connection with exponential backoff
     * Max out at the time specified by RECONNECT_TIMER_MAX_TIME_MILLISECONDS (15 minutes)
     */
    private void retryBillingClientConnection() {
        long currentDelay = reconnectMilliseconds.get();
        findUiHandler().postDelayed(this::connect, currentDelay);

        long currentVal, newVal;
        do {
            currentVal = reconnectMilliseconds.get();
            newVal = Math.min(currentVal * 2, RECONNECT_TIMER_MAX_TIME_MILLISECONDS);
        } while (!reconnectMilliseconds.compareAndSet(currentVal, newVal));
    }

    /**
     * Fires a query in Play Console to show products available to purchase
     */
    private void queryProductDetails(String productType, List<QueryProductDetailsParams.Product> productList) {
        QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(productList).build();

        billingClient.queryProductDetailsAsync(productDetailsParams, (billingResult, productDetailsResult) -> {
            if (billingResult.getResponseCode() == OK) {
                List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();

                HashSet<String> foundProductIds = new HashSet<>();
                for (ProductDetails details : productDetailsList) {
                    foundProductIds.add(details.getProductId());
                }

                for (QueryProductDetailsParams.Product requestedProduct : productList) {
                    String productId = requestedProduct.zza(); // .zza() gets the product ID string
                    if (!foundProductIds.contains(productId)) {
                        Log("Error: Product ID '" + productId + "' not found. " +
                                "Make sure it is configured correctly in the Play Console");
                        findUiHandler().post(() -> billingEventListener.onProductQueryError(productId, new BillingResponse(ErrorType.PRODUCT_ID_QUERY_FAILED,
                                "Product ID '" + productId + "' not found", defaultResponseCode)
                        ));
                    }
                }

                if (productDetailsList.isEmpty()) {
                    Log("Query Product Details: No valid products found. Make sure product ids are configured on Play Console");
                    findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.BILLING_ERROR,
                            "No products found", defaultResponseCode)));
                } else {
                    Log("Query Product Details: data found for " + productDetailsList.size() + " products");

                    List<ProductInfo> fetchedProductInfo = new ArrayList<>();
                    for (ProductDetails productDetails : productDetailsList) {
                        fetchedProductInfo.add(generateProductInfo(productDetails));
                    }
                    fetchedProductInfoList.addAll(fetchedProductInfo);

                    switch (productType) {
                        case INAPP:
                        case SUBS:
                            findUiHandler().post(() -> billingEventListener.onProductsFetched(fetchedProductInfo));
                            break;
                        default:
                            throw new IllegalStateException("Product type is not implemented");
                    }

                    if (--productDetailsQueriesPending == 0) {
                        fetchPurchasedProducts();
                    }
                }
            } else {
                Log("Query Product Details: failed with response code: " + billingResult.getResponseCode());
                findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                        new BillingResponse(ErrorType.BILLING_ERROR, billingResult)));
            }
        });
    }

    /**
     * Returns a new ProductInfo object containing the product type and product details
     *
     * @param productDetails - is the object provided by the billing client API
     */
    @NonNull
    private ProductInfo generateProductInfo(@NonNull ProductDetails productDetails) {
        SkuProductType skuProductType;

        switch (productDetails.getProductType()) {
            case INAPP:
                boolean consumable = isProductIdConsumable(productDetails.getProductId());
                if (consumable) {
                    skuProductType = SkuProductType.CONSUMABLE;
                } else {
                    skuProductType = SkuProductType.NON_CONSUMABLE;
                }
                break;
            case SUBS:
                skuProductType = SkuProductType.SUBSCRIPTION;
                break;
            default:
                throw new IllegalStateException("Product type is not implemented correctly");
        }

        return new ProductInfo(skuProductType, productDetails);
    }

    private boolean isProductIdConsumable(String productId) {
        if (consumableIds == null) {
            return false;
        }

        return consumableIds.contains(productId);
    }

    /**
     * Returns purchases details for currently owned items without a network request
     */
    private void fetchPurchasedProducts() {
        if (billingClient.isReady()) {
            purchaseQueriesPending = 1;
            if (isSubscriptionSupported() == SupportState.SUPPORTED) {
                purchaseQueriesPending++;
            }

            billingClient.queryPurchasesAsync(
                    QueryPurchasesParams.newBuilder().setProductType(INAPP).build(),
                    (billingResult, purchases) -> {
                        if (billingResult.getResponseCode() == OK) {
                            if (purchases.isEmpty()) {
                                Log("Query IN-APP Purchases: the list is empty");
                            } else {
                                Log("Query IN-APP Purchases: data found and progress");
                            }

                            processPurchases(ProductType.INAPP, purchases, true);
                        } else {
                            Log("Query IN-APP Purchases: failed");
                        }
                    }
            );

            //query subscription purchases for supported devices
            if (isSubscriptionSupported() == SupportState.SUPPORTED) {
                billingClient.queryPurchasesAsync(
                        QueryPurchasesParams.newBuilder().setProductType(SUBS).build(),
                        (billingResult, purchases) -> {
                            if (billingResult.getResponseCode() == OK) {
                                if (purchases.isEmpty()) {
                                    Log("Query SUBS Purchases: the list is empty");
                                } else {
                                    Log("Query SUBS Purchases: data found and progress");
                                }

                                processPurchases(ProductType.SUBS, purchases, true);
                            } else {
                                Log("Query SUBS Purchases: failed");
                            }
                        }
                );
            }

        } else {
            findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.FETCH_PURCHASED_PRODUCTS_ERROR,
                    "Billing client is not ready yet", defaultResponseCode)));
        }
    }

    /**
     * Before using subscriptions, device-support must be checked
     * Not all devices support subscriptions
     */
    public SupportState isSubscriptionSupported() {
        BillingResult response = billingClient.isFeatureSupported(SUBSCRIPTIONS);
        SupportState state;

        switch (response.getResponseCode()) {
            case OK:
                Log("Subscriptions support check: success");
                state = SupportState.SUPPORTED;
                break;
            case SERVICE_DISCONNECTED:
                Log("Subscriptions support check: disconnected. Trying to reconnect...");
                state = SupportState.DISCONNECTED;
                break;
            default:
                Log("Subscriptions support check: error -> " + response.getResponseCode() + " " + response.getDebugMessage());
                state = SupportState.NOT_SUPPORTED;
                break;
        }
        return state;
    }

    /**
     * Checks purchases signature for more security
     */
    private void processPurchases(ProductType productType, @NonNull List<Purchase> allPurchases, boolean purchasedProductsFetched) {
        List<PurchaseInfo> signatureValidPurchases = new ArrayList<>();

        List<Purchase> validPurchases = new ArrayList<>();
        for (Purchase purchase : allPurchases) {
            if (isPurchaseSignatureValid(purchase)) {
                validPurchases.add(purchase);
            }
        }

        Map<String, ProductInfo> productInfoMap = new HashMap<>();
        for (ProductInfo productInfo : fetchedProductInfoList) {
            productInfoMap.put(productInfo.getProduct(), productInfo);
        }

        for (Purchase purchase : validPurchases) {
            for (String productId : purchase.getProducts()) {
                ProductInfo foundProductInfo = productInfoMap.get(productId);
                if (foundProductInfo != null) {
                    PurchaseInfo purchaseInfo = new PurchaseInfo(foundProductInfo, purchase);
                    signatureValidPurchases.add(purchaseInfo);
                }
            }
        }

        //synchronize access to purchasedProductsList
        synchronized (purchasedProductsSync) {
            //clear existing purchases of this type when fetching (to avoid duplicates)
            if (purchasedProductsFetched) {
                Iterator<PurchaseInfo> iterator = purchasedProductsList.iterator();
                while (iterator.hasNext()) {
                    PurchaseInfo purchaseInfo = iterator.next();
                    boolean isSubscription = purchaseInfo.getSkuProductType() == SkuProductType.SUBSCRIPTION;

                    if (productType == ProductType.SUBS && isSubscription) {
                        iterator.remove();
                    } else if (productType == ProductType.INAPP && !isSubscription) {
                        iterator.remove();
                    } else if (productType == ProductType.COMBINED) {
                        iterator.remove();
                    }
                }
            }

            //add new purchases
            purchasedProductsList.addAll(signatureValidPurchases);
        }

        if (purchasedProductsFetched) {
            findUiHandler().post(() -> billingEventListener.onPurchasedProductsFetched(productType, signatureValidPurchases));
            if (--purchaseQueriesPending == 0) {
                fetchedPurchasedProducts = true;
            }
        } else {
            findUiHandler().post(() -> billingEventListener.onProductsPurchased(signatureValidPurchases));
        }

        for (PurchaseInfo purchaseInfo : signatureValidPurchases) {
            if (shouldAutoConsume) {
                consumePurchase(purchaseInfo);
            }
            if (shouldAutoAcknowledge) {
                boolean isProductConsumable = purchaseInfo.getSkuProductType() == SkuProductType.CONSUMABLE;
                if (!isProductConsumable) {
                    acknowledgePurchase(purchaseInfo);
                }
            }
        }
    }

    /**
     * Consume consumable products so that the user can buy the item again
     * <p>
     * Consumable products might be bought/consumed by users multiple times (for eg. diamonds, coins etc)
     * They have to be consumed within 3 days otherwise Google will refund the products
     */
    public void consumePurchase(@NonNull PurchaseInfo purchaseInfo) {
        if (checkProductBeforeInteraction(purchaseInfo.getProduct())) {
            if (purchaseInfo.getSkuProductType() == SkuProductType.CONSUMABLE) {
                if (purchaseInfo.getPurchase().getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
                    ConsumeParams consumeParams = ConsumeParams.newBuilder()
                            .setPurchaseToken(purchaseInfo.getPurchase().getPurchaseToken()).build();

                    billingClient.consumeAsync(consumeParams, (billingResult, purchaseToken) -> {
                        if (billingResult.getResponseCode() == OK) {
                            synchronized (purchasedProductsSync) {
                                purchasedProductsList.remove(purchaseInfo);
                            }
                            findUiHandler().post(() -> billingEventListener.onPurchaseConsumed(purchaseInfo));
                        } else {
                            Log("Handling consumables: error during consumption attempt: " + billingResult.getDebugMessage());

                            findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
                                    new BillingResponse(ErrorType.CONSUME_ERROR, billingResult)));
                        }
                    });
                } else if (purchaseInfo.getPurchase().getPurchaseState() == Purchase.PurchaseState.PENDING) {
                    Log("Handling consumables: purchase can not be consumed because the state is PENDING. " +
                            "A purchase can be consumed only when the state is PURCHASED");

                    findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.CONSUME_WARNING,
                            "Warning: purchase can not be consumed because the state is PENDING. Please consume the purchase later", defaultResponseCode)));
                }
            }
        }
    }

    /**
     * Acknowledge non-consumable products & subscriptions
     * <p>
     * This will avoid refunding for these products to users by Google
     */
    public void acknowledgePurchase(@NonNull PurchaseInfo purchaseInfo) {
        if (checkProductBeforeInteraction(purchaseInfo.getProduct())) {
            switch (purchaseInfo.getSkuProductType()) {
                case NON_CONSUMABLE:
                case SUBSCRIPTION:
