package io.layers.sdk.reactnative;

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.WritableMap;

/**
 * Native module for fetching the Google Play install referrer.
 *
 * Uses the Install Referrer API via reflection to avoid a hard dependency on
 * `com.android.installreferrer:installreferrer`. If the library is not present
 * in the host app, the module resolves with null.
 */
public class LayersInstallReferrerModule extends ReactContextBaseJavaModule {

    /** Name of the worker thread that reads ReferrerDetails off the main thread. */
    private static final String WORKER_THREAD_NAME = "LayersInstallReferrer";

    public LayersInstallReferrerModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "LayersInstallReferrer";
    }

    /**
     * Read a no-arg {@code long} getter off ReferrerDetails, or 0 when the
     * installed installreferrer version predates it.
     */
    private static long longField(Object details, String getter) {
        try {
            return (Long) details.getClass().getMethod(getter).invoke(details);
        } catch (Exception e) {
            return 0L;
        }
    }

    /** Same, for a {@code String} getter. Missing or null yields "". */
    private static String stringField(Object details, String getter) {
        try {
            Object value = details.getClass().getMethod(getter).invoke(details);
            return value != null ? (String) value : "";
        } catch (Exception e) {
            return "";
        }
    }

    /** Same, for a {@code boolean} getter. Missing yields false. */
    private static boolean booleanField(Object details, String getter) {
        try {
            return (Boolean) details.getClass().getMethod(getter).invoke(details);
        } catch (Exception e) {
            return false;
        }
    }

    @ReactMethod
    public void getInstallReferrer(Promise promise) {
        try {
            // Use reflection to avoid hard dependency on installreferrer library
            Class<?> clientClass = Class.forName(
                    "com.android.installreferrer.api.InstallReferrerClient");
            java.lang.reflect.Method newBuilder = clientClass.getMethod(
                    "newBuilder", android.content.Context.class);
            Object builder = newBuilder.invoke(null, getReactApplicationContext());

            java.lang.reflect.Method buildMethod = builder.getClass().getMethod("build");
            final Object client = buildMethod.invoke(builder);

            // Get the listener interface class
            Class<?> listenerClass = Class.forName(
                    "com.android.installreferrer.api.InstallReferrerStateListener");

            // Guard against double-resolve: onDisconnected arrives when the Play
            // Store's process dies, which can happen while the worker is reading
            final java.util.concurrent.atomic.AtomicBoolean resolved =
                    new java.util.concurrent.atomic.AtomicBoolean(false);

            // Create a dynamic proxy for the listener
            Object listener = java.lang.reflect.Proxy.newProxyInstance(
                    listenerClass.getClassLoader(),
                    new Class<?>[]{ listenerClass },
                    (proxy, method, args) -> {
                        if ("onInstallReferrerSetupFinished".equals(method.getName())) {
                            int responseCode = (Integer) args[0];
                            if (responseCode == 0) { // OK
                                // The Play library calls this listener on the main thread,
                                // and getInstallReferrer() is a synchronous binder call into
                                // the Play Store. At first launch the Play Store can still be
                                // finishing the install and hold that call past 5 s, which
                                // Android reports as an ANR. The read runs on a worker and
                                // this listener returns at once.
                                startReferrerRead(client, resolved, promise);
                            } else {
                                if (resolved.compareAndSet(false, true)) {
                                    promise.resolve(null);
                                }
                            }
                        } else if ("onInstallReferrerServiceDisconnected".equals(method.getName())) {
                            // Service disconnected before we could get the referrer
                            if (resolved.compareAndSet(false, true)) {
                                promise.resolve(null);
                            }
                        }
                        return null;
                    }
            );

            java.lang.reflect.Method startConnection = client.getClass()
                    .getMethod("startConnection", listenerClass);
            startConnection.invoke(client, listener);
        } catch (ClassNotFoundException e) {
            // installreferrer library not available
            promise.resolve(null);
        } catch (Exception e) {
            promise.resolve(null);
        }
    }

    /**
     * Run {@link #readReferrer} on a one-shot daemon thread, handing it the
     * client captured when the connection started. A device short on memory
     * can refuse a new native thread; that settles the fetch with null and
     * closes the connection here.
     */
    private static void startReferrerRead(final Object client,
            final java.util.concurrent.atomic.AtomicBoolean resolved, final Promise promise) {
        try {
            Thread worker = new Thread(
                    () -> readReferrer(client, resolved, promise), WORKER_THREAD_NAME);
            worker.setDaemon(true);
            worker.start();
        } catch (Throwable t) {
            if (resolved.compareAndSet(false, true)) {
                promise.resolve(null);
            }
            endConnection(client);
        }
    }

    /**
     * Read ReferrerDetails off the connected client, settle the promise, then
     * close the connection. Runs on the worker thread, since
     * getInstallReferrer() is binder IPC into the Play Store. Every failure
     * (a disconnected client, a missing method, an Error) settles with null,
     * so the promise always completes and the worker never throws.
     */
    private static void readReferrer(Object client,
            java.util.concurrent.atomic.AtomicBoolean resolved, Promise promise) {
        try {
            java.lang.reflect.Method getDetails = client.getClass()
                    .getMethod("getInstallReferrer");
            Object details = getDetails.invoke(client);

            java.lang.reflect.Method getUrl = details.getClass()
                    .getMethod("getInstallReferrer");
            java.lang.reflect.Method getClickTs = details.getClass()
                    .getMethod("getReferrerClickTimestampSeconds");
            java.lang.reflect.Method getInstallTs = details.getClass()
                    .getMethod("getInstallBeginTimestampSeconds");

            String referrerUrl = (String) getUrl.invoke(details);
            long clickTs = (Long) getClickTs.invoke(details);
            long installTs = (Long) getInstallTs.invoke(details);

            WritableMap result = Arguments.createMap();
            result.putString("referrerUrl", referrerUrl != null ? referrerUrl : "");
            result.putDouble("referrerClickTimestamp", clickTs);
            result.putDouble("installBeginTimestamp", installTs);

            // The remaining four ReferrerDetails fields are what the
            // canonical `install_referrer` event carries alongside the
            // three above (schema/fixtures/install-referrer-event.json).
            // They arrived in installreferrer 1.1, so each is read
            // independently with its own fallback: on an older library
            // the JS side still gets a complete map with 0 / "" / false,
            // and a missing method costs only its own field.
            result.putDouble("referrerClickTimestampServer",
                    longField(details, "getReferrerClickTimestampServerSeconds"));
            result.putDouble("installBeginTimestampServer",
                    longField(details, "getInstallBeginTimestampServerSeconds"));
            result.putString("installVersion",
                    stringField(details, "getInstallVersion"));
            result.putBoolean("googlePlayInstant",
                    booleanField(details, "getGooglePlayInstantParam"));
            if (resolved.compareAndSet(false, true)) {
                promise.resolve(result);
            }
        } catch (Throwable t) {
            if (resolved.compareAndSet(false, true)) {
                promise.resolve(null);
            }
        } finally {
            endConnection(client);
        }
    }

    /** Close the client's connection by reflection. Failures are ignored. */
    private static void endConnection(Object client) {
        try {
            java.lang.reflect.Method endConn = client.getClass()
                    .getMethod("endConnection");
            endConn.invoke(client);
        } catch (Throwable ignored) {}
    }
}
