package com.ludei.inapps.googleplay;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;

import com.ludei.inapps.*;
import com.android.vending.billing.*;

import org.json.JSONException;
import org.json.JSONObject;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;

public class GooglePlayInAppService extends AbstractInAppService
{
    public static class GPInAppPurchase extends InAppPurchase {
        public String signature;
        public String developerPayload;
        public String purchaseToken;
        public int purchaseState;
        public String purchaseData;

        public static GPInAppPurchase from(String purchaseData, String signature) throws JSONException {

            JSONObject jo = new JSONObject(purchaseData);
            GPInAppPurchase purchase = new GPInAppPurchase();
            purchase.signature = signature;
            purchase.purchaseData = purchaseData;
            purchase.productId = jo.getString("productId");
            purchase.transactionId = jo.getString("orderId");
            purchase.quantity = 1;
            purchase.purchaseState = jo.getInt("purchaseState");
            purchase.purchaseToken = jo.getString("purchaseToken");
            purchase.developerPayload = jo.optString("developerPayload");
            purchase.purchaseDate = new Date(jo.optLong("purchaseTime"));
            return purchase;
        }
    }

    private IInAppBillingService mService;
    private InitCompletion mServiceOnConnected;
    private ServiceConnection mServiceConn = new ServiceConnection() {
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
            if (mServiceOnConnected != null) {
                mServiceOnConnected.onInit(new Error(0, "Service Disconnected"));
                mServiceOnConnected = null;
            }
        }

        @Override
        public void onServiceConnected(ComponentName name,
                                       IBinder service) {
            mService = IInAppBillingService.Stub.asInterface(service);
            if (mServiceOnConnected != null) {
                mServiceOnConnected.onInit(null);
                mServiceOnConnected = null;
            }
        }
    };
    private String mPendingIntentProductId;
    private String developerPayload = UUID.randomUUID().toString();
    private int apiVersion = 3;


    public GooglePlayInAppService(Context ctx)
    {
        super(ctx);
        Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
        serviceIntent.setPackage("com.android.vending");
        mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
    }


    @Override
    public void init(InitCompletion callback)
    {
        if (callback == null) {
            return;
        }

        if (mService != null) {
            callback.onInit(null);
        }
        else {
            mServiceOnConnected = callback;
        }
    }

    @Override
    public void onDestroy() {
        if (mService != null) {
            mContext.unbindService(mServiceConn);
        }
    }

    private InAppProduct JSONObjectToInapp(JSONObject object) {
        InAppProduct product = new InAppProduct();

        product.productId = object.optString("productId");
        //String type = object.optString("type");
        product.localizedPrice = object.optString("price");
        product.title = object.optString("title");
        product.description = object.optString("description");
        String price;
        if (object.has("price_amount_micros")) {
            price = String.valueOf(((float) object.optInt("price_amount_micros")) / 1000000);

        } else {
            String tmpPrice = product.localizedPrice.replace(",", ".");
            price = String.valueOf(tmpPrice.replace(',', '.').substring(0, tmpPrice.length() - 2));
        }
        product.price = new BigDecimal(price).doubleValue();
        return product;
    }

    public void internalFetchProducts(final List<String> productIds, final FetchCallback callback)
    {

        if (mService == null) {
            callback.onComplete(null, new Error(0, "Service disconnected"));
            return;
        }

        runBackgroundTask(new Runnable() {
            @Override
            public void run() {
                final int MAX_SKU = 20; //Google Play limit getSkuDetails

                Error error = null;
                final ArrayList<InAppProduct> products = new ArrayList<InAppProduct>();

                ArrayList<String> pids = new ArrayList<String>(productIds);
                while (pids.size() > 0) {
                    List<String> currentQuery = pids.size() <= MAX_SKU ? pids : pids.subList(0, MAX_SKU);
                    Bundle querySkus = new Bundle();
                    querySkus.putStringArrayList("ITEM_ID_LIST", new ArrayList<String>(currentQuery));
                    currentQuery.clear();

                    try {
                        Bundle skuDetails = mService.getSkuDetails(apiVersion, mContext.getPackageName(), "inapp", querySkus);

                        int response = skuDetails.getInt("RESPONSE_CODE");
                        if (response == 0) {
                            ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");

                            for (String productResponse : responseList) {
                                products.add(JSONObjectToInapp(new JSONObject(productResponse)));
                            }
                        } else {
                            error = new Error(response, Utils.getResponseDesc(response));
                        }
                    } catch (Exception ex) {
                        error = new Error(0, ex.toString());
                    }
                }

                final Error finalError = error;
                dispatchCallback(new Runnable() {
                    @Override
                    public void run() {
                        callback.onComplete(products, finalError);
                    }
                });
            }
        });
    }


    @Override
    public boolean canPurchase() {

        try {
            return mService!= null && mService.isBillingSupported(3, mContext.getPackageName(), "inapp") == 0;
        } catch (RemoteException e) {
            return false;
        }
    }

    final private int BUY_INTENT_REQUEST_CODE = 1104389;


    protected void handleAlreadyOwnedError(final String productId) {
        //try to hide the already owned error and simulate a successful purchase
        this.fetchPurchases(productId, 0, new FetchPurchasesCallback() {
            @Override
            public void onCompleted(ArrayList<GPInAppPurchase> purchases, final Error error) {
                 if (error != null || (purchases == null || purchases.size() == 0)) {
                     dispatchCallback(new Runnable() {
                         @Override
                         public void run() {
                            String innerMessage = error != null ? error.message : "Empty purchase array";
                            notifyPurchaseFailed(productId, new Error(0, "Error while trying to restore an already owned item: " + innerMessage));
                         }
                     });
                 }
                else {
                     validatePurchase(purchases.get(0), null);
                }
            }
        });
    }

    @Override
    public void purchase(final String productId, int quantity, final PurchaseCallback callback) {

        if (mService == null) {
            if (callback != null) {
                callback.onComplete(null, new Error(0, "Service disconnected"));
            }
            return;
        }

        if (callback != null) {
            mPurchaseCallbacks.put(productId, callback);
        }
        dispatchCallback(new Runnable() {
            @Override
            public void run() {
                notifyPurchaseStarted(productId);
            }
        });
        try {
            mPendingIntentProductId = productId;
            Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), productId, "inapp", developerPayload);
            final int code = buyIntentBundle.getInt("RESPONSE_CODE", 0);

            if (code == Utils.ResponseCode.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) {
                handleAlreadyOwnedError(productId);
                return;
            }
            else if (code != Utils.ResponseCode.BILLING_RESPONSE_RESULT_OK) {
                dispatchCallback(new Runnable() {
                    @Override
                    public void run() {
                        notifyPurchaseFailed(productId, new Error(code, Utils.getResponseDesc(code)));
                    }
                });
                return;
            }
            PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
            ((Activity)mContext).startIntentSenderForResult(pendingIntent.getIntentSender(), BUY_INTENT_REQUEST_CODE, new Intent(), 0, 0, 0);

        }
        catch (final Exception e) {
            dispatchCallback(new Runnable() {
                @Override
                public void run() {
                    notifyPurchaseFailed(productId, new Error(0, e.toString()));
                }
            });
        }

    }

    @Override
    public void consume(final String productId, int quantity, final ConsumeCallback callback)
    {
        if (mService == null) {
            if (callback != null) {
                callback.onComplete(0, new Error(0, "Service disconnected"));
            }
            return;
        }

        fetchPurchases(productId, 0, new FetchPurchasesCallback() {
            @Override
            public void onCompleted(ArrayList<GPInAppPurchase> purchases, final Error error) {

                Error consumeError = error;
                int consumed = 0;
                if (error == null && purchases != null && purchases.size() > 0) {
                    GPInAppPurchase purchase = purchases.get(0);
                    try {
                        int response = mService.consumePurchase(apiVersion, mContext.getPackageName(), purchase.purchaseToken);
                        if (response == 0) {
                            consumed = 1;
                        }
                        else {
                            consumeError = Utils.getResponseError(response);
                        }
                    }
                    catch (Exception e) {
                        consumeError = new Error(e);
                    }

                }
                final Error finalError = consumeError;
                final int finalConsumed = consumed;
                dispatchCallback(new Runnable() {
                    @Override
                    public void run() {
                        if (callback != null) {
                            if (finalConsumed > 0) {
                                mStock.put(productId, 0);
                                saveCipheredStock();
                            }
                            callback.onComplete(finalConsumed, finalError);
                        }
                    }
                });

            }
        });
    }

    protected void validatePurchase(final GPInAppPurchase purchase, final PurchaseCallback callback) {
        //validate developer payload
        if (!this.developerPayload.equals(purchase.developerPayload)) {
            dispatchCallback(new Runnable() {
                @Override
                public void run() {
                    Error error = new Error(0, "Validation failed, developerPayload doesn't match: " + purchase.developerPayload);
                    notifyPurchaseFailed(purchase.productId, error);
                    if (callback != null) {
                        callback.onComplete(purchase, error);
                    }
                }
            });
            return;
        }

        //local validation or server validation
        StringBuilder unverifiedData = new StringBuilder()
                .append("{\"signedData\": " + purchase.purchaseData)
                .append(", ")
                .append("\"signature\": \"" + purchase.signature + "\"}");

        super.validate(unverifiedData.toString(), purchase.productId, new ValidationCompletion() {
            @Override
            public void finishValidation(final Error error) {
                dispatchCallback(new Runnable() {
                    @Override
                    public void run() {
                        if (error != null) {
                            //validation failed
                            notifyPurchaseFailed(purchase.productId, error);
                        }
                        else {
                            //validation completed, update stock and save it.
                            mStock.put(purchase.productId, 1);
                            saveCipheredStock();
                            notifyPurchaseCompleted(purchase);
                        }
                        if (callback != null) {
                            callback.onComplete(purchase, error);
                        }
                    }
                });
            }
        });

    }

    @Override
    public boolean onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode != BUY_INTENT_REQUEST_CODE) {
            return false;
        }

        int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
        String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
        String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
        final String productId = mPendingIntentProductId;
        mPendingIntentProductId = null;

        if (responseCode != Utils.ResponseCode.BILLING_RESPONSE_RESULT_OK) {

            if (responseCode == Utils.ResponseCode.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) {
                handleAlreadyOwnedError(productId);
                return true;
            }
            Error error = new Error(responseCode, Utils.getResponseDesc(responseCode));
            notifyPurchaseFailed(productId, error);
        }
        else if (resultCode != Activity.RESULT_OK) {
            notifyPurchaseFailed(productId, new Error(resultCode, "Activity result not RESULT_OK"));
            return true;
        }
        else {
            try {
                GPInAppPurchase purchase = GPInAppPurchase.from(purchaseData, dataSignature);
                this.validatePurchase(purchase, null);
            } catch (JSONException e) {
                notifyPurchaseFailed(productId, new Error(0, e.toString()));
            }
        }

        return true;
    }

    public interface FetchPurchasesCallback {
        void onCompleted(ArrayList<GPInAppPurchase> purchases, Error error);
    }
    public void fetchPurchases(final String filterProductId, final int filterState, final FetchPurchasesCallback callback) {

        runBackgroundTask(new Runnable() {
            @Override
            public void run() {

                String continuationToken = null;
                try {

                    ArrayList<GPInAppPurchase> purchases = new ArrayList<GPInAppPurchase>();
                    while (true) {
                        Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), "inapp", continuationToken);
                        final int response = ownedItems.getInt("RESPONSE_CODE");

                        if (response != 0) {
                            callback.onCompleted(purchases, new Error(response, Utils.getResponseDesc(response)));
                            return;
                        }
                        ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
                        ArrayList<String> purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
                        ArrayList<String> signatureList = ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
                        continuationToken = ownedItems.getString("INAPP_CONTINUATION_TOKEN");

                        for (int i = 0; i < purchaseDataList.size(); ++i) {
                            String sku = ownedSkus.get(i);
                            if (filterProductId != null && !filterProductId.equals(sku)) {
                                continue;
                            }
                            String purchaseData = purchaseDataList.get(i);
                            String signature = signatureList.get(i);
                            GPInAppPurchase purchase = GPInAppPurchase.from(purchaseData, signature);
                            purchase.developerPayload = developerPayload;
                            if (filterState <0 || filterState == purchase.purchaseState) {
                                purchases.add(purchase);
                            }

                        }
                        if (continuationToken == null || continuationToken.length() == 0) {
                            callback.onCompleted(purchases, null);
                            break;
                        }
                    }

                }
                catch (final Exception e) {
                    callback.onCompleted(null, new Error(0, e.toString()));
                }
            }
        });
    }

    @Override
    public void restorePurchases(final RestoreCallback callback) {
        if (mService == null) {
            if (callback != null) {
                callback.onComplete(new Error(0, "Service disconnected"));
            }
            return;
        }

        this.fetchPurchases(null, 0, new FetchPurchasesCallback() {
            @Override
            public void onCompleted(ArrayList<GPInAppPurchase> purchases, final Error error) {

                if (error != null) {
                    dispatchCallback(new Runnable() {
                        @Override
                        public void run() {
                            if (callback != null) {
                                callback.onComplete(error);
                            }
                        }
                    });
                    return;
                }

                final int[] pending = new int[]{purchases.size()};

                for (final GPInAppPurchase purchase: purchases) {
                    dispatchCallback(new Runnable() {
                        @Override
                        public void run() {
                            notifyPurchaseStarted(purchase.productId);
                        }
                    });
                    validatePurchase(purchase, new PurchaseCallback() {
                        @Override
                        public void onComplete(InAppPurchase purchase, Error error) {

                            pending[0]--;
                            //notify restore completion when all the validations are processed
                            if (pending[0] <=0) {
                                if (callback != null) {
                                    callback.onComplete(null);
                                }

                            }
                        }
                    });
                }

            }
        });

    }

    @Override
    public void setLudeiServerValidationHandler() {
        setValidationHandler(new LudeiServerValidation(this, 1));
    }

}