package io.sentry;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;

import java.io.File;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.sentry.android.core.AnrIntegration;
import io.sentry.android.core.NdkIntegration;
import io.sentry.android.core.SentryAndroid;
import io.sentry.core.Integration;
import io.sentry.core.SentryOptions;
import io.sentry.core.UncaughtExceptionHandlerIntegration;
import io.sentry.core.protocol.SentryException;

@ReactModule(name = RNSentryModule.NAME)
public class RNSentryModule extends ReactContextBaseJavaModule {

    public static final String NAME = "RNSentry";

    final static Logger logger = Logger.getLogger("react-native-sentry");

    private static PackageInfo packageInfo;
    private SentryOptions sentryOptions;

    public RNSentryModule(ReactApplicationContext reactContext) {
        super(reactContext);
        RNSentryModule.packageInfo = getPackageInfo(reactContext);
    }

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

    @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();
        constants.put("nativeClientAvailable", true);
        constants.put("nativeTransport", true);
        return constants;
    }

    @ReactMethod
    public void startWithOptions(final ReadableMap rnOptions, Promise promise) {
        SentryAndroid.init(this.getReactApplicationContext(), options -> {
            if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) {
                String dsn = rnOptions.getString("dsn");
                logger.info(String.format("Starting with DSN: '%s'", dsn));
                options.setDsn(dsn);
            } else {
                // SentryAndroid needs an empty string fallback for the dsn.
                options.setDsn("");
            }
            if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) {
                options.setDebug(true);
            }
            if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) {
                options.setEnvironment(rnOptions.getString("environment"));
            }
            if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) {
                options.setRelease(rnOptions.getString("release"));
            }
            if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) {
                options.setDist(rnOptions.getString("dist"));
            }
            if (rnOptions.hasKey("enableAutoSessionTracking") && rnOptions.getBoolean("enableAutoSessionTracking")) {
                options.setEnableSessionTracking(true);
            }

            // JS use top level stacktraces and android attaches Threads which hides them so by default we hide.
            boolean attachThreads = rnOptions.hasKey("attachThreads") && rnOptions.getBoolean("attachThreads");
            options.setAttachThreads(attachThreads);

            options.setBeforeSend((event, hint) -> {
                // React native internally throws a JavascriptException
                // Since we catch it before that, we don't want to send this one
                // because we would send it twice
                try {
                    SentryException ex = event.getExceptions().get(0);
                    if (null != ex && ex.getType().contains("JavascriptException")) {
                        return null;
                    }
                } catch (Exception e) {
                    // We do nothing
                }
                return event;
            });

            for (Iterator<Integration> iterator = options.getIntegrations().iterator(); iterator.hasNext();) {
                Integration integration = iterator.next();
                if (rnOptions.hasKey("enableNativeCrashHandling")
                        && !rnOptions.getBoolean("enableNativeCrashHandling")) {
                    if (integration instanceof UncaughtExceptionHandlerIntegration
                            || integration instanceof AnrIntegration || integration instanceof NdkIntegration) {
                        iterator.remove();
                    }
                }
            }

            logger.info(String.format("Native Integrations '%s'", options.getIntegrations().toString()));
            sentryOptions = options;
        });

        promise.resolve(true);
    }

    @ReactMethod
    public void setLogLevel(int level) {
        logger.setLevel(this.logLevel(level));
    }

    @ReactMethod
    public void crash() {
        throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)");
    }

    @ReactMethod
    public void fetchRelease(Promise promise) {
        WritableMap release = Arguments.createMap();
        release.putString("id", packageInfo.packageName);
        release.putString("version", packageInfo.versionName);
        release.putString("build", String.valueOf(packageInfo.versionCode));
        promise.resolve(release);
    }

    @ReactMethod
    public void captureEnvelope(String envelope, Promise promise) {
        try {
            File installation = new File(sentryOptions.getOutboxPath(), UUID.randomUUID().toString());
            try (FileOutputStream out = new FileOutputStream(installation)) {
                out.write(envelope.getBytes(Charset.forName("UTF-8")));
            }
        } catch (Exception e) {
            logger.info("Error reading envelope");
        }
        promise.resolve(true);
    }

    @ReactMethod
    public void getStringBytesLength(String payload, Promise promise) {
        try {
            promise.resolve(payload.getBytes("UTF-8").length);
        } catch (UnsupportedEncodingException e) {
            promise.reject(e);
        }
    }

    private static PackageInfo getPackageInfo(Context ctx) {
        try {
            return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            logger.info("Error getting package info.");
            return null;
        }
    }

    private Level logLevel(int level) {
        switch (level) {
            case 1:
                return Level.SEVERE;
            case 2:
                return Level.INFO;
            case 3:
                return Level.ALL;
            default:
                return Level.OFF;
        }
    }
}

@@ -714,16 +714,39 @@
 erator;%0A
+import java.util.List;%0A
 import j
@@ -4326,103 +4326,131 @@
     
-for (Iterator%3CIntegration%3E iterator = options.getIntegrations().iterator(); iterator.hasNext();
+if (rnOptions.hasKey(%22enableNativeCrashHandling%22)%0A                    && !rnOptions.getBoolean(%22enableNativeCrashHandling%22)
 ) %7B%0A
@@ -4465,16 +4465,27 @@
         
+final List%3C
 Integrat
@@ -4487,16 +4487,17 @@
 egration
+%3E
  integra
@@ -4504,24 +4504,35 @@
 tion
+s
  = 
-i
+options.getIn
 te
+g
 rat
-or.next
+ions
 ();%0A
@@ -4551,135 +4551,57 @@
     
-if (rnOptions.hasKey(%22enableNativeCrashHandling%22)%0A                        && !rnOptions.getBoolean(%22enableNativeCrashHandling%22)
+for (final Integration integration : integrations
 ) %7B%0A
@@ -4678,24 +4678,27 @@
 rIntegration
+ %7C%7C
 %0A           
@@ -4713,19 +4713,16 @@
         
- %7C%7C
  integra
@@ -4754,16 +4754,44 @@
 ation %7C%7C
+%0A                           
  integra
@@ -4853,23 +4853,27 @@
    i
+n
 te
+g
 rat
-or
+ions
 .remove(
 );%0A 
@@ -4868,16 +4868,27 @@
 .remove(
+integration
 );%0A     

disable native integration (#875)