package expo.modules.payments.stripe;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;

import com.google.android.gms.wallet.WalletConstants;
import com.stripe.android.ApiResultCallback;
import com.stripe.android.Stripe;
import com.stripe.android.model.Source;
import com.stripe.android.model.Source.Flow;
import com.stripe.android.model.SourceParams;
import com.stripe.android.model.Token;

import org.unimodules.core.ExportedModule;
import org.unimodules.core.ModuleRegistry;
import org.unimodules.core.Promise;
import org.unimodules.core.interfaces.ActivityEventListener;
import org.unimodules.core.interfaces.ActivityProvider;
import org.unimodules.core.interfaces.ExpoMethod;
import org.unimodules.core.interfaces.services.UIManager;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import expo.modules.payments.stripe.dialog.AddCardDialogFragment;
import expo.modules.payments.stripe.util.ArgCheck;
import expo.modules.payments.stripe.util.Converters;
import expo.modules.payments.stripe.util.Fun0;

import static expo.modules.payments.stripe.Errors.getDescription;
import static expo.modules.payments.stripe.Errors.getErrorCode;
import static expo.modules.payments.stripe.Errors.toErrorCode;
import static expo.modules.payments.stripe.util.Converters.convertSourceToWritableMap;
import static expo.modules.payments.stripe.util.Converters.convertTokenToWritableMap;
import static expo.modules.payments.stripe.util.Converters.createBankAccountTokenParams;
import static expo.modules.payments.stripe.util.Converters.getStringOrNull;
import static expo.modules.payments.stripe.util.InitializationOptions.ANDROID_PAY_MODE_KEY;
import static expo.modules.payments.stripe.util.InitializationOptions.ANDROID_PAY_MODE_PRODUCTION;
import static expo.modules.payments.stripe.util.InitializationOptions.ANDROID_PAY_MODE_TEST;
import static expo.modules.payments.stripe.util.InitializationOptions.PUBLISHABLE_KEY;

public class StripeModule extends ExportedModule {
  private static final String META_DATA_SCHEME_KEY = "standaloneStripeScheme";
  private static final String MODULE_NAME = StripeModule.class.getSimpleName();
  private static HashMap<Integer, WeakReference<StripeModule>> sMapOfInstances = new HashMap<>();

  private Context mContext;
  private ModuleRegistry mModuleRegistry = null;

  public static StripeModule getInstance(int tag) {
    WeakReference<StripeModule> res = sMapOfInstances.get(tag);
    if(res == null) {
      return null;
    }
    return res.get();
  }

  public static int getLastTag() {
    return sCounter.get();
  }

  public Stripe getStripe() {
    return mStripe;
  }

  @Nullable
  private Promise mCreateSourcePromise;

  @Nullable
  private Source mCreatedSource;

  private String mPublicKey;
  private Stripe mStripe;
  private PayFlow mPayFlow;
  private Map<String, Object> mErrorCodes;

  private int mTag = 0;
  private static final AtomicInteger sCounter = new AtomicInteger(0);


  private final ActivityEventListener mActivityEventListener = new ActivityEventListener() {
    @Override
    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
      boolean handled = getPayFlow().onActivityResult(requestCode, resultCode, data);
    }

    @Override
    public void onNewIntent(Intent intent) {
      // Do nothing...
    }
  };


  public StripeModule(Context context) {
    super(context);
    this.mContext = context;
    mTag = sCounter.incrementAndGet();
    sMapOfInstances.put(mTag, new WeakReference<StripeModule>(this));
  }

  @Override
  public String getName() {
    return MODULE_NAME;
  }

  @ExpoMethod
  public void init(@NonNull Map<String, Object> options, @NonNull Map<String, Object> errorCodes, Promise promise) {
    ArgCheck.nonNull(options);

    String newPubKey = Converters.getStringOrNull(options, PUBLISHABLE_KEY);
    String newAndroidPayMode = Converters.getStringOrNull(options, ANDROID_PAY_MODE_KEY);

    if (newPubKey != null && !TextUtils.equals(newPubKey, mPublicKey)) {
      ArgCheck.notEmptyString(newPubKey);

      mPublicKey = newPubKey;
      mStripe = new Stripe(mContext, mPublicKey);
      getPayFlow().setPublishableKey(mPublicKey);
    }

    if (newAndroidPayMode != null) {
      ArgCheck.isTrue(ANDROID_PAY_MODE_TEST.equals(newAndroidPayMode) || ANDROID_PAY_MODE_PRODUCTION.equals(newAndroidPayMode));

      getPayFlow().setEnvironment(androidPayModeToEnvironment(newAndroidPayMode));
    }

    if (mErrorCodes == null) {
      mErrorCodes = errorCodes;
      getPayFlow().setErrorCodes(errorCodes);
    }

    promise.resolve(null);
  }

  private PayFlow getPayFlow() {
    if (mPayFlow == null) {
      mPayFlow = PayFlow.create(
        new Fun0<Activity>() { public Activity call() {
          return getCurrentActivity();
        }}
      );
    }

    return mPayFlow;
  }

  private Activity getCurrentActivity() {
    return mModuleRegistry.getModule(ActivityProvider.class).getCurrentActivity();
  }

  private static int androidPayModeToEnvironment(@NonNull String androidPayMode) {
    ArgCheck.notEmptyString(androidPayMode);
    return ANDROID_PAY_MODE_TEST.equals(androidPayMode.toLowerCase()) ? WalletConstants.ENVIRONMENT_TEST : WalletConstants.ENVIRONMENT_PRODUCTION;
  }

  @ExpoMethod
  public void deviceSupportsAndroidPay(final Promise promise) {
    getPayFlow().deviceSupportsAndroidPay(false, promise);
  }

  @ExpoMethod
  public void canMakeAndroidPayPayments(final Promise promise) {
    getPayFlow().deviceSupportsAndroidPay(true, promise);
  }

  @ExpoMethod
  public void createTokenWithCard(final Map<String, Object> cardData, final Promise promise) {
    try {
      ArgCheck.nonNull(mStripe);
      ArgCheck.notEmptyString(mPublicKey);

      mStripe.createCardToken(
        Converters.createCardParams(cardData),
        null,
        null,
        new ApiResultCallback<Token>() {
          @Override
          public void onSuccess(@NonNull Token token) {
            promise.resolve(convertTokenToWritableMap(token));
          }
          @Override
          public void onError(Exception error) {
            error.printStackTrace();
            promise.reject(toErrorCode(error), error.getMessage());
          }
        });
    } catch (Exception e) {
      promise.reject(toErrorCode(e), e.getMessage());
    }
  }

  @ExpoMethod
  public void createTokenWithBankAccount(final Map<String, Object> accountData, final Promise promise) {
    try {
      ArgCheck.nonNull(mStripe);
      ArgCheck.notEmptyString(mPublicKey);

      mStripe.createBankAccountToken(
        createBankAccountTokenParams(accountData),
        mPublicKey,
        null,
        new ApiResultCallback<Token>() {
          @Override
          public void onSuccess(@NonNull Token token) {
            promise.resolve(convertTokenToWritableMap(token));
          }
          @Override
          public void onError(Exception error) {
            error.printStackTrace();
            promise.reject(toErrorCode(error), error.getMessage());
          }
        });
    } catch (Exception e) {
      promise.reject(toErrorCode(e), e.getMessage());
    }
  }

  @ExpoMethod
  public void paymentRequestWithCardForm(Map<String, Object> params, final Promise promise) {
    Activity currentActivity = getCurrentActivity();
    try {
      ArgCheck.nonNull(currentActivity);
      ArgCheck.notEmptyString(mPublicKey);

      boolean createCardSource = params.get("createCardSource") instanceof Boolean ? (Boolean) params.get("createCardSource") : false;

      final AddCardDialogFragment cardDialog = AddCardDialogFragment.newInstance(
          mPublicKey,
          getErrorCode(mErrorCodes, "cancelled"),
          getDescription(mErrorCodes, "cancelled"),
          createCardSource,
          mTag
      );
      cardDialog.setPromise(promise);
      cardDialog.show(currentActivity.getFragmentManager(), "AddNewCard");
    } catch (Exception e) {
      promise.reject(toErrorCode(e), e.getMessage());
    }
  }

  @ExpoMethod
  public void paymentRequestWithAndroidPay(final Map<String, Object> payParams, final Promise promise) {
    getPayFlow().paymentRequestWithAndroidPay(payParams, promise);
  }

  @ExpoMethod
  public void createSourceWithParams(final Map<String, Object> options, final Promise promise) {
    String sourceType = (String)options.get("type");
    SourceParams sourceParams = null;

    if (isPropertyEmpty(options.get("returnURL"))) {
      Application application = getCurrentActivity().getApplication();
      Context context =  application.getApplicationContext();
      String prefix = getClass().getPackage().getName();
      try {
        ApplicationInfo applicationInfo = application.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
        Bundle bundle = applicationInfo.metaData;
        String standaloneScheme = bundle.getString(META_DATA_SCHEME_KEY);
        if (standaloneScheme != null) {
          prefix += "." + standaloneScheme;
        }
      } catch (Exception e) {
        // custom standaloneStripeScheme doesn't exist
        // using the default scheme - package name
      }
      String newReturnURL = prefix + "://" + mTag;
      options.put("returnURL", newReturnURL);
    }

    switch (sourceType) {
      case "alipay":
        sourceParams = SourceParams.createAlipaySingleUseParams(
            Math.round((Double)options.get("amount")),
            (String)options.get("currency"),
            getStringOrNull(options, "name"),
            getStringOrNull(options, "email"),
            (String)options.get("returnURL"));
        break;
      case "bancontact":
        sourceParams = SourceParams.createBancontactParams(
            Math.round((Double)options.get("amount")),
            (String)options.get("name"),
            (String)options.get("returnURL"),
            getStringOrNull(options, "statementDescriptor"),
            (String)options.get("preferredLanguage"));
        break;
      case "giropay":
        sourceParams = SourceParams.createGiropayParams(
            Math.round((Double)options.get("amount")),
            (String)options.get("name"),
            (String)options.get("returnURL"),
            getStringOrNull(options, "statementDescriptor"));
        break;
      case "ideal":
        sourceParams = SourceParams.createIdealParams(
            Math.round((Double)options.get("amount")),
            (String)options.get("name"),
            (String)options.get("returnURL"),
            getStringOrNull(options, "statementDescriptor"),
            getStringOrNull(options, "bank"));
        break;
      case "sepaDebit":
        sourceParams = SourceParams.createSepaDebitParams(
            (String)options.get("name"),
            (String)options.get("iban"),
            getStringOrNull(options, "addressLine1"),
            (String)options.get("city"),
            (String)options.get("postalCode"),
            (String)options.get("country"));
        break;
      case "sofort":
        sourceParams = SourceParams.createSofortParams(
            Math.round((Double)options.get("amount")),
            (String)options.get("returnURL"),
            (String)options.get("country"),
            getStringOrNull(options, "statementDescriptor"));
        break;
      case "threeDSecure":
        sourceParams = SourceParams.createThreeDSecureParams(
            Math.round(((Double)options.get("amount"))),
            (String)options.get("currency"),
            (String)options.get("returnURL"),
            (String)options.get("card"));
        break;
    }

    ArgCheck.nonNull(sourceParams);

    mStripe.createSource(sourceParams, new ApiResultCallback<Source>() {
      @Override
      public void onError(Exception error) {
        promise.reject(toErrorCode(error), error);
      }

      @Override
      public void onSuccess(@NonNull Source source) {
        if (Flow.Redirect.equals(source.getFlow())) {
          Activity currentActivity = getCurrentActivity();
          if (currentActivity == null) {
            promise.reject(
              getErrorCode(mErrorCodes, "activityUnavailable"),
              getDescription(mErrorCodes, "activityUnavailable")
            );
          } else {
            mCreateSourcePromise = promise;
            mCreatedSource = source;
            String redirectUrl = source.getRedirect().getUrl();
            Intent browserIntent = new Intent(currentActivity, OpenBrowserActivity.class)
                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)
                .putExtra(OpenBrowserActivity.EXTRA_URL, redirectUrl)
                .putExtra("tag", mTag);
            currentActivity.startActivity(browserIntent);
          }
        } else {
          promise.resolve(convertSourceToWritableMap(source));
        }
      }
    });
  }

  private boolean isPropertyEmpty(Object property) {
    if (property == null) {
      return true;
    }
    if (property instanceof String) {
      String stringProp = (String) property;
      return stringProp.equals("");
    }
    return false;
  }

  void processRedirect(@Nullable Uri redirectData) {
    if (mCreatedSource == null || mCreateSourcePromise == null) {

      return;
    }

    if (redirectData == null) {

      mCreateSourcePromise.reject(
        getErrorCode(mErrorCodes, "redirectCancelled"),
        getDescription(mErrorCodes, "redirectCancelled")
      );
      mCreatedSource = null;
      mCreateSourcePromise = null;
      return;
    }

    final String clientSecret = redirectData.getQueryParameter("client_secret");
    if (!mCreatedSource.getClientSecret().equals(clientSecret)) {
      mCreateSourcePromise.reject(
        getErrorCode(mErrorCodes, "redirectNoSource"),
        getDescription(mErrorCodes, "redirectNoSource")
      );
      mCreatedSource = null;
      mCreateSourcePromise = null;
      return;
    }

    final String sourceId = redirectData.getQueryParameter("source");
    if (!mCreatedSource.getId().equals(sourceId)) {
      mCreateSourcePromise.reject(
        getErrorCode(mErrorCodes, "redirectWrongSourceId"),
        getDescription(mErrorCodes, "redirectWrongSourceId")
      );
      mCreatedSource = null;
      mCreateSourcePromise = null;
      return;
    }

    final Promise promise = mCreateSourcePromise;

    // Nulls those properties to avoid processing them twice
    mCreatedSource = null;
    mCreateSourcePromise = null;

    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(Void... voids) {
        Source source = null;
        try {
          source = mStripe.retrieveSourceSynchronous(sourceId, clientSecret);
        } catch (Exception e) {

          return null;
        }

        switch (source.getStatus()) {
          case Chargeable:
          case Consumed:
            promise.resolve(convertSourceToWritableMap(source));
            break;
          case Canceled:
            promise.reject(
              getErrorCode(mErrorCodes, "redirectCancelled"),
              getDescription(mErrorCodes, "redirectCancelled")
            );
            break;
          case Pending:
          case Failed:
            promise.reject(
              getErrorCode(mErrorCodes, "redirectFailed"),
              getDescription(mErrorCodes, "redirectFailed")
            );
        }
        return null;
      }
    }.execute();
  }

  @Override
  public void onCreate(ModuleRegistry moduleRegistry) {
    this.mModuleRegistry = moduleRegistry;

    // Add the listener for `onActivityResult`
    mModuleRegistry.getModule(UIManager.class).registerActivityEventListener(mActivityEventListener);
  }
}
