package com.conviva.react.apptracker;

import com.conviva.apptracker.ConvivaAppAnalytics;
import com.conviva.apptracker.configuration.Configuration;
import com.conviva.apptracker.configuration.EmitterConfiguration;
import com.conviva.apptracker.configuration.GdprConfiguration;
import com.conviva.apptracker.configuration.GlobalContextsConfiguration;
import com.conviva.apptracker.configuration.NetworkConfiguration;
import com.conviva.apptracker.configuration.RemoteConfiguration;
import com.conviva.apptracker.configuration.SessionConfiguration;
import com.conviva.apptracker.configuration.SessionReplayConfiguration;
import com.conviva.apptracker.configuration.SubjectConfiguration;
import com.conviva.apptracker.configuration.TrackerConfiguration;
import com.conviva.apptracker.controller.TrackerController;
import com.conviva.apptracker.internal.tracker.ClidSyncConfiguration;
import com.conviva.apptracker.event.ButtonClick;
import com.conviva.apptracker.event.ConsentGranted;
import com.conviva.apptracker.revenue.ConvivaRevenueEvent;
import com.conviva.apptracker.revenue.ConvivaRevenueEventItem;
import com.conviva.apptracker.event.ConsentWithdrawn;
import com.conviva.apptracker.event.DeepLinkReceived;
import com.conviva.apptracker.event.EcommerceTransaction;
import com.conviva.apptracker.event.MessageNotification;
import com.conviva.apptracker.event.PageView;
import com.conviva.apptracker.event.ScreenView;
import com.conviva.apptracker.event.SelfDescribing;
import com.conviva.apptracker.event.Structured;
import com.conviva.apptracker.event.Timing;
import com.conviva.apptracker.internal.constants.TrackerConstants;
import com.conviva.apptracker.network.CollectorCookieJar;
import com.conviva.apptracker.network.HttpMethod;
import com.conviva.apptracker.payload.SelfDescribingJson;
import com.conviva.apptracker.util.Size;
import com.conviva.react.apptracker.util.ConfigUtil;
import com.conviva.react.apptracker.util.EventUtil;
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.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;

import org.json.JSONObject;
import org.json.simple.JSONValue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.Request;

import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarker.MarkerListener;
import com.facebook.react.bridge.ReactMarkerConstants;


public class RNConvivaTrackerModule extends ReactContextBaseJavaModule {

    private final ReactApplicationContext reactContext;

    public static volatile long contentAppearedTimestamp;


    public RNConvivaTrackerModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;

        addContentAppearedMarkerListener();
    }

    private void addContentAppearedMarkerListener() {
        if (contentAppearedTimestamp <= 0) {
            ReactMarker.addListener(new MarkerListener() {
                @Override
                public void logMarker(ReactMarkerConstants name, String tag, int instanceKey) {
                    if (name == ReactMarkerConstants.CONTENT_APPEARED && contentAppearedTimestamp <= 0) {
                        contentAppearedTimestamp = System.currentTimeMillis();
                        TrackerController tracker = ConvivaAppAnalytics.getDefaultTracker();
                        if (tracker != null) {
                            tracker.setContentAppearedTimestamp(contentAppearedTimestamp);
                        }
                    }
                }
            });
        }
    }

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

    @ReactMethod
    public void createTracker(ReadableMap argmap, Promise promise) {
        try {
            String appName = argmap.getString("appName");
            String customerKey = argmap.getString("customerKey");

            // Configurations
            List<Configuration> controllers = new ArrayList<Configuration>();

            // NetworkConfiguration
            NetworkConfiguration networkConfiguration;
            if (argmap.hasKey("networkConfig")) {
                ReadableMap networkConfig = argmap.getMap("networkConfig");
                String endpoint = TrackerConstants.CONVIVA_PRODUCTION_ENDPOINT;
                if (networkConfig.hasKey("endpoint") && !networkConfig.isNull("endpoint")) {
                    endpoint = networkConfig.getString("endpoint");
                }
                if (networkConfig.hasKey("method") && !networkConfig.isNull("method")) {
                    String method = networkConfig.getString("method");
                    networkConfiguration = new NetworkConfiguration(endpoint, ("get".equalsIgnoreCase(method) ? HttpMethod.GET : HttpMethod.POST));
                } else {
                    networkConfiguration = new NetworkConfiguration(networkConfig.getString("endpoint"));
                }
                if (networkConfig.hasKey("customPostPath") && !networkConfig.isNull("customPostPath")) {
                    String customPostPath = networkConfig.getString("customPostPath");
                    networkConfiguration.customPostPath = customPostPath;
                }
                if (networkConfig.hasKey("requestHeaders") && !networkConfig.isNull("requestHeaders")) {
                    ReadableMap requestHeaders = networkConfig.getMap("requestHeaders");
                    if (requestHeaders != null) {
                        OkHttpClient client = new OkHttpClient.Builder().connectTimeout(15, TimeUnit.SECONDS).readTimeout(15, TimeUnit.SECONDS).cookieJar(new CollectorCookieJar(reactContext)).addInterceptor(chain -> {
                            Request.Builder requestBuilder = chain.request().newBuilder();
                            ReadableMapKeySetIterator it = requestHeaders.keySetIterator();
                            while (it.hasNextKey()) {
                                String key = it.nextKey();
                                String value = requestHeaders.getString(key);
                                if (value != null) {
                                    requestBuilder.header(key, value);
                                }
                            }
                            return chain.proceed(requestBuilder.build());
                        }).build();
                        networkConfiguration.okHttpClient(client);
                    }
                }
                controllers.add(networkConfiguration);
            }

            // TrackerConfiguration
            // Need to create the TrackerConfiguration to update the postfix for the tracker version
            ReadableMap trackerConfig = null;
            if (argmap.hasKey("trackerConfig")) {
                trackerConfig = argmap.getMap("trackerConfig");
            }
            TrackerConfiguration trackerConfiguration = ConfigUtil.mkTrackerConfiguration(trackerConfig, appName, this.reactContext);
            controllers.add(trackerConfiguration);

            // SessionConfiguration
            if (argmap.hasKey("sessionConfig")) {
                ReadableMap sessionConfig = argmap.getMap("sessionConfig");
                SessionConfiguration sessionConfiguration = ConfigUtil.mkSessionConfiguration(sessionConfig);
                controllers.add(sessionConfiguration);
            }

            // EmitterConfiguration
            if (argmap.hasKey("emitterConfig")) {
                ReadableMap emitterConfig = argmap.getMap("emitterConfig");
                EmitterConfiguration emitterConfiguration = ConfigUtil.mkEmitterConfiguration(emitterConfig);
                controllers.add(emitterConfiguration);
            }

            // SubjectConfiguration
            if (argmap.hasKey("subjectConfig")) {
                ReadableMap subjectConfig = argmap.getMap("subjectConfig");
                SubjectConfiguration subjectConfiguration = ConfigUtil.mkSubjectConfiguration(subjectConfig);
                controllers.add(subjectConfiguration);
            }

            // GdprConfiguration
            if (argmap.hasKey("gdprConfig")) {
                ReadableMap gdprConfig = argmap.getMap("gdprConfig");
                GdprConfiguration gdprConfiguration = ConfigUtil.mkGdprConfiguration(gdprConfig);
                controllers.add(gdprConfiguration);
            }

            // GCConfiguration
            if (argmap.hasKey("gcConfig")) {
                ReadableArray gcConfig = argmap.getArray("gcConfig");
                GlobalContextsConfiguration gcConfiguration = ConfigUtil.mkGCConfiguration(gcConfig);
                controllers.add(gcConfiguration);
            }

            // ClidSyncConfiguration
            if (argmap.hasKey("clidSyncConfig")) {
                ReadableMap clidSyncConfig = argmap.getMap("clidSyncConfig");
                ClidSyncConfiguration clidSyncConfiguration = ConfigUtil.mkClidSyncConfiguration(clidSyncConfig);
                controllers.add(clidSyncConfiguration);
            }

            // SessionReplayConfiguration
            if (argmap.hasKey("sessionReplayConfig")) {
                ReadableMap src = argmap.getMap("sessionReplayConfig");
                if (src != null) {
                    SessionReplayConfiguration replayConfiguration = ConfigUtil.mkSessionReplayConfiguration(src);
                    if (replayConfiguration != null) controllers.add(replayConfiguration);
                }
            }

            String remoteConfigUrl = RemoteConfiguration.DEFAULT_ENDPOINT;
            if (argmap.hasKey("remoteConfiguration")) {
                ReadableMap remoteConfig = argmap.getMap("remoteConfiguration");
                if (remoteConfig.hasKey("endpoint")) {
                    remoteConfigUrl = remoteConfig.getString("endpoint");
                }
            }

            // create the tracker
            TrackerController trackerController = ConvivaAppAnalytics.createTracker(this.reactContext, customerKey, appName, controllers.toArray(new Configuration[controllers.size()]));

            //Set the contentAppearedTimestamp if already captured
            if(trackerController != null && contentAppearedTimestamp > 0) {
                trackerController.setContentAppearedTimestamp(contentAppearedTimestamp);
            }

            promise.resolve(true);

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void removeTracker(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                promise.resolve(ConvivaAppAnalytics.removeTracker(trackerController));
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void cleanup(Promise promise) {
        try {
            // commented out deprecated methods
            promise.resolve(true);

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }
    @ReactMethod
    public void removeAllTrackers(Promise promise) {
        try {
            // commented out deprecated methods
            // ConvivaAppAnalytics.removeAllTrackers();
            promise.resolve(true);

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setClientId(ReadableMap argmap, Promise promise) {
        try {
            promise.resolve(ConvivaAppAnalytics.setClientId(this.reactContext, argmap.getString("clientId")));
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void getClientId(Promise promise) {
        try {
            promise.resolve(ConvivaAppAnalytics.getClientId(this.reactContext));
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackSelfDescribingEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                SelfDescribingJson sdj = EventUtil.createSelfDescribingJson(argmap);
                SelfDescribing event = new SelfDescribing(sdj);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackStructuredEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                Structured event = EventUtil.createStructuredEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackScreenViewEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                ScreenView event = EventUtil.createScreenViewEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackPageView(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                PageView event = EventUtil.createPageViewEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackTimingEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");

                Timing event = EventUtil.createTimingEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackConsentGrantedEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                ConsentGranted event = EventUtil.createConsentGrantedEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackConsentWithdrawnEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                ConsentWithdrawn event = EventUtil.createConsentWithdrawnEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackEcommerceTransactionEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                EcommerceTransaction event = EventUtil.createEcommerceTransactionEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackDeepLinkReceivedEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                DeepLinkReceived event = EventUtil.createDeepLinkReceivedEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackMessageNotificationEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");
                ReadableArray contexts = details.getArray("contexts");


                MessageNotification event = EventUtil.createMessageNotificationEvent(argmap);

                List<SelfDescribingJson> evCtxts = EventUtil.createContexts(contexts);
                event.customContexts.addAll(evCtxts);

                trackerController.track(event);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackCustomEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                String eventName = details.getString("eventName");
                ReadableMap argmap = details.getMap("eventData");

                HashMap<String, Object> eventData = argmap.toHashMap();

                trackerController.trackCustomEvent(eventName, JSONValue.toJSONString(eventData));
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackRevenueEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("eventData");

                double totalOrderAmount = argmap.getDouble("totalOrderAmount");
                String transactionId = argmap.getString("transactionId");
                String currency = argmap.getString("currency");

                ConvivaRevenueEvent.Builder builder =
                    ConvivaRevenueEvent.builder(totalOrderAmount, transactionId, currency);

                if (argmap.hasKey("taxAmount") && !argmap.isNull("taxAmount"))
                    builder.taxAmount(argmap.getDouble("taxAmount"));
                if (argmap.hasKey("shippingCost") && !argmap.isNull("shippingCost"))
                    builder.shippingCost(argmap.getDouble("shippingCost"));
                if (argmap.hasKey("discount") && !argmap.isNull("discount"))
                    builder.discount(argmap.getDouble("discount"));
                if (argmap.hasKey("cartSize") && !argmap.isNull("cartSize"))
                    builder.cartSize(argmap.getInt("cartSize"));
                if (argmap.hasKey("paymentMethod") && !argmap.isNull("paymentMethod"))
                    builder.paymentMethod(argmap.getString("paymentMethod"));
                if (argmap.hasKey("paymentProvider") && !argmap.isNull("paymentProvider"))
                    builder.paymentProvider(argmap.getString("paymentProvider"));
                if (argmap.hasKey("extraMetadata") && !argmap.isNull("extraMetadata"))
                    builder.extraMetadata(new JSONObject(argmap.getMap("extraMetadata").toHashMap()));

                if (argmap.hasKey("items") && !argmap.isNull("items")) {
                    ReadableArray itemsArray = argmap.getArray("items");
                    List<ConvivaRevenueEventItem> items = new ArrayList<>();
                    for (int i = 0; i < itemsArray.size(); i++) {
                        ReadableMap itemDict = itemsArray.getMap(i);
                        ConvivaRevenueEventItem.Builder itemBuilder = ConvivaRevenueEventItem.builder();
                        if (itemDict.hasKey("productId") && !itemDict.isNull("productId"))
                            itemBuilder.productId(itemDict.getString("productId"));
                        if (itemDict.hasKey("name") && !itemDict.isNull("name"))
                            itemBuilder.name(itemDict.getString("name"));
                        if (itemDict.hasKey("sku") && !itemDict.isNull("sku"))
                            itemBuilder.sku(itemDict.getString("sku"));
                        if (itemDict.hasKey("category") && !itemDict.isNull("category"))
                            itemBuilder.category(EventUtil.createStrings(itemDict.getArray("category")));
                        if (itemDict.hasKey("unitPrice") && !itemDict.isNull("unitPrice"))
                            itemBuilder.unitPrice(itemDict.getDouble("unitPrice"));
                        if (itemDict.hasKey("quantity") && !itemDict.isNull("quantity"))
                            itemBuilder.quantity(itemDict.getInt("quantity"));
                        if (itemDict.hasKey("discount") && !itemDict.isNull("discount"))
                            itemBuilder.discount(itemDict.getDouble("discount"));
                        if (itemDict.hasKey("brand") && !itemDict.isNull("brand"))
                            itemBuilder.brand(itemDict.getString("brand"));
                        if (itemDict.hasKey("variant") && !itemDict.isNull("variant"))
                            itemBuilder.variant(itemDict.getString("variant"));
                        if (itemDict.hasKey("extraMetadata") && !itemDict.isNull("extraMetadata"))
                            itemBuilder.extraMetadata(new JSONObject(itemDict.getMap("extraMetadata").toHashMap()));
                        items.add(itemBuilder.build());
                    }
                    builder.items(items);
                }

                trackerController.trackRevenueEvent(builder.build());
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setCustomTags(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap argmap = details.getMap("tags");

                HashMap<String, Object> tags = argmap.toHashMap();

                trackerController.setCustomTags(tags);
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void clearCustomTags(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableArray argArray = details.getArray("tagKeys");


                List<String> tagKeys = EventUtil.createStrings(argArray);
                trackerController.clearCustomTags(new HashSet<>(tagKeys));
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void clearAllCustomTags(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                trackerController.clearAllCustomTags();
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void trackClickEvent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap eventMap = details.getMap("eventData");


                ButtonClick event = EventUtil.createButtonClickEvent(eventMap);
                trackerController.track(event);

                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void removeGlobalContexts(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods            
            // String namespace = details.getString("tracker");
            // String tag = details.getString("removeTag");

            // TrackerController trackerController = getTracker(namespace);

            // trackerController.getGlobalContexts().remove(tag);
            promise.resolve(true);

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void addGlobalContexts(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods   
            // String namespace = details.getString("tracker");
            // ReadableMap gcArg = details.getMap("addGlobalContext");

            // String tag = gcArg.getString("tag");
            // ReadableArray globalContexts = gcArg.getArray("globalContexts");

            // List<SelfDescribingJson> staticContexts = new ArrayList<>();
            // for (int i = 0; i < globalContexts.size(); i++) {
            //     SelfDescribingJson gContext = EventUtil.createSelfDescribingJson(globalContexts.getMap(i));
            //     staticContexts.add(gContext);
            // }
            // GlobalContext gcStatic = new GlobalContext(staticContexts);

            // TrackerController trackerController = getTracker(namespace);

            // trackerController.getGlobalContexts().add(tag, gcStatic);
            promise.resolve(true);

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setUserId(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("userId")) {
                    trackerController.getSubject().setUserId(null);
                } else {
                    trackerController.getSubject().setUserId(details.getString("userId"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setNetworkUserId(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("networkUserId")) {
                    trackerController.getSubject().setNetworkUserId(null);
                } else {
                    trackerController.getSubject().setNetworkUserId(details.getString("networkUserId"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setDomainUserId(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("domainUserId")) {
                    trackerController.getSubject().setDomainUserId(null);
                } else {
                    trackerController.getSubject().setDomainUserId(details.getString("domainUserId"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setIpAddress(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("ipAddress")) {
                    trackerController.getSubject().setIpAddress(null);
                } else {
                    trackerController.getSubject().setIpAddress(details.getString("ipAddress"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setUseragent(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("useragent")) {
                    trackerController.getSubject().setUseragent(null);
                } else {
                    trackerController.getSubject().setUseragent(details.getString("useragent"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setTimezone(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("timezone")) {
                    trackerController.getSubject().setTimezone(null);
                } else {
                    trackerController.getSubject().setTimezone(details.getString("timezone"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setLanguage(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("language")) {
                    trackerController.getSubject().setLanguage(null);
                } else {
                    trackerController.getSubject().setLanguage(details.getString("language"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setScreenResolution(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("screenResolution")) {
                    trackerController.getSubject().setScreenResolution(null);
                } else {
                    ReadableArray screenRes = details.getArray("screenResolution");
                    int width = screenRes.getInt(0);
                    int height = screenRes.getInt(1);
                    Size screenR = new Size(width, height);

                    trackerController.getSubject().setScreenResolution(screenR);
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setScreenViewport(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("screenViewport")) {
                    trackerController.getSubject().setScreenViewPort(null);
                } else {
                    ReadableArray screenView = details.getArray("screenViewport");
                    int width = screenView.getInt(0);
                    int height = screenView.getInt(1);
                    Size screenVP = new Size(width, height);

                    trackerController.getSubject().setScreenViewPort(screenVP);
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void setColorDepth(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {

                if (details.isNull("colorDepth")) {
                    trackerController.getSubject().setColorDepth(null);
                } else {
                    trackerController.getSubject().setColorDepth(details.getInt("colorDepth"));
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }

        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void getSessionUserId(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods
            // String namespace = details.getString("tracker");
            // TrackerController trackerController = getTracker(namespace);

            // String suid = trackerController.getSession().getUserId();
            // promise.resolve(suid);
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void getSessionId(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods
            // String namespace = details.getString("tracker");
            // TrackerController trackerController = getTracker(namespace);

            // String sid = trackerController.getSession().getSessionId();
            // promise.resolve(sid);
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void getSessionIndex(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods
            // String namespace = details.getString("tracker");
            // TrackerController trackerController = getTracker(namespace);

            // int sidx = trackerController.getSession().getSessionIndex();
            // promise.resolve(sidx);
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void getIsInBackground(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods
            // String namespace = details.getString("tracker");
            // TrackerController trackerController = getTracker(namespace);

            // boolean isInBg = trackerController.getSession().isInBackground();
            // promise.resolve(isInBg);
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void getBackgroundIndex(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods
            // String namespace = details.getString("tracker");
            // TrackerController trackerController = getTracker(namespace);

            // int bgIdx = trackerController.getSession().getBackgroundIndex();
            // promise.resolve(bgIdx);
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void getForegroundIndex(ReadableMap details, Promise promise) {
        try {
            // commented out deprecated methods
            // String namespace = details.getString("tracker");
            // TrackerController trackerController = getTracker(namespace);

            // int fgIdx = trackerController.getSession().getForegroundIndex();
            // promise.resolve(fgIdx);
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    private void setContentAppearedTimestamp(ReadableMap argmap, Promise promise) {
        try {
            String namespace = argmap.getString("tracker");
            long contentAppearedTimestamp = (long) argmap.getDouble("contentAppearedTimestamp");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                trackerController.setContentAppearedTimestamp(contentAppearedTimestamp);
            }
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void startReplay(Promise promise) {
        try {
            ConvivaAppAnalytics.startReplay();
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void stopReplay(Promise promise) {
        try {
            ConvivaAppAnalytics.stopReplay();
            promise.resolve(true);
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    private TrackerController getTracker(String namespace) {
        return namespace == null ? ConvivaAppAnalytics.getDefaultTracker() : ConvivaAppAnalytics.getTracker(namespace);
    }

    /**
     * JS → native JS-bundle-identity entry point.
     *
     * Forwards the JS bundle identity map (jsBundleSource / jsBundleId /
     * jsBundleChannel / jsBundleLabel / jsEngine) to the native SDK so it can be
     * attached to subsequent events.
     *
     * Never throws.
     */
    @ReactMethod
    public void setJsBundleInfo(ReadableMap details, Promise promise) {
        try {
            String namespace = details.getString("tracker");
            TrackerController trackerController = getTracker(namespace);
            if (trackerController != null) {
                ReadableMap infoMap = details.getMap("info");
                if (infoMap != null) {
                    trackerController.setJsBundleInfo(infoMap.toHashMap());
                }
                promise.resolve(true);
            } else {
                promise.reject("ERROR", "TrackerController is null");
            }
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    /**
     * Returns the verbatim remote-config JSON last applied by the native SDK
     * (from cache, default, or a remote fetch), or null if none has been
     * applied yet. The JS layer parses this to enable/disable error-tracking
     * features and reconfigure the rate limiter — the native side performs no
     * error-tracking gating. D7: never throws (rejects only on unexpected
     * failure so the JS caller can fall back to its defaults).
     */
    @ReactMethod
    public void getRemoteConfig(Promise promise) {
        try {
            promise.resolve(ConvivaAppAnalytics.getAppliedRemoteConfigJson());
        } catch (Throwable t) {
            promise.reject("ERROR", t.getMessage());
        }
    }

    @ReactMethod
    public void reportJsError(String payloadJson) {
        try {
            if (payloadJson == null || payloadJson.length() == 0) {
                return;
            }
            JSONObject json = new JSONObject(payloadJson);

            boolean isFatal = json.optBoolean("isFatal", false);

            TrackerController trackerController = getTracker("CAT");
            if (trackerController == null) {
                trackerController = ConvivaAppAnalytics.getDefaultTracker();
            }
            if (trackerController == null) {
                return;
            }

            //Fatal errors are tracked by the native SDK's ExceptionHandler.
            if (!isFatal) {
                // Non-fatal JS error — custom event with descriptive keys.
                // This is the dedicated metric surface that does not affect
                // crash-rate dashboards.
                HashMap<String, Object> eventData = new HashMap<>();
                eventData.put("message",        json.optString("message", ""));
                eventData.put("errorType",       json.optString("errorType", ""));
                eventData.put("stackTrace",      json.optString("stackTrace", ""));
                eventData.put("timestamp",       json.optLong("timestamp", 0L));
                eventData.put("isFatal",         false);
                eventData.put("isHandled",       json.optBoolean("isHandled", true));
                eventData.put("errorSource",     json.optString("errorSource", ""));
                eventData.put("severity",        json.optString("severity", ""));
                eventData.put("componentStack",  json.optString("componentStack", ""));
                eventData.put("jsEngine",        json.optString("jsEngine", "unknown"));

                if (json.has("lineNumber") && !json.isNull("lineNumber")) {
                    eventData.put("lineNumber", json.opt("lineNumber"));
                }
                if (json.has("lineColumn") && !json.isNull("lineColumn")) {
                    eventData.put("lineColumn", json.opt("lineColumn"));
                }
                if (json.has("fileName") && !json.isNull("fileName")) {
                    eventData.put("fileName", json.opt("fileName"));
                }
                if (json.has("bundleId") && !json.isNull("bundleId")) {
                    eventData.put("bundleId", json.opt("bundleId"));
                }

                // Consumer attributes — all keys not in the known set.
                HashSet<String> knownKeys = new HashSet<>(Arrays.asList(
                    "timestamp", "message", "errorType", "stackTrace",
                    "isFatal", "isHandled", "errorSource", "severity",
                    "componentStack", "jsEngine",
                    "lineNumber", "lineColumn", "fileName", "bundleId"
                ));
                Iterator<String> keys = json.keys();
                while (keys.hasNext()) {
                    String key = keys.next();
                    if (!knownKeys.contains(key)) {
                        eventData.put(key, json.opt(key));
                    }
                }

                trackerController.trackCustomEvent("conviva_non_fatal_error",
                    JSONValue.toJSONString(eventData));
            }
        } catch (Throwable t) {
            // D7: error-reporting path must never throw.
        }
    }
}
