package com.contentsquare.rn;

import static com.contentsquare.rn.utils.MapUtil.convertAndAddToCustomVarList;
import static com.contentsquare.rn.utils.MapUtil.getValueForKey;

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.contentsquare.android.Contentsquare;
import com.contentsquare.android.api.bridge.xpf.ExternalBridgeInterface;
import com.contentsquare.android.api.bridge.xpf.ExternalBridgeType;
import com.contentsquare.android.api.model.CustomVar;
import com.contentsquare.android.api.model.Transaction;
import com.contentsquare.rn.eventEmitter.CSEventEmitterModuleImpl;
import com.contentsquare.rn.externalbridge.ExternalBridgeInterfaceImpl;
import com.contentsquare.rn.externalbridge.XpfInterfaceBridge;
import com.contentsquare.rn.utils.LogMonitorUtil;
import com.contentsquare.rn.utils.ReactNativeUiThreadUtil;
import com.contentsquare.rn.webview.WebViewInjector;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
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 com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableMap;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ContentsquareModuleImpl {
    public static final String NAME = "ContentsquareModule";

    @NonNull
    private final ReactApplicationContext mReactContext;
    @NonNull
    private final WebViewInjector mWebViewInjector;
    @NonNull
    private final CSEventEmitterModuleImpl mCSEventEmitterModuleImpl;
    @NonNull
    private final ReactNativeUiThreadUtil mReactNativeUiThreadUtil;

    private final ExternalBridgeInterface externalBridgeInterface;

    public ContentsquareModuleImpl(@NonNull final ReactApplicationContext reactContext,
            @NonNull final WebViewInjector webViewInjector,
            CSEventEmitterModuleImpl csEventEmitterModuleImpl,
            @NonNull ReactNativeUiThreadUtil reactNativeUiThreadUtil) {
        super();
        mReactContext = reactContext;
        mWebViewInjector = webViewInjector;
        mCSEventEmitterModuleImpl = csEventEmitterModuleImpl;
        mReactNativeUiThreadUtil = reactNativeUiThreadUtil;
        XpfInterfaceBridge xpfInterfaceBridge = XpfInterfaceBridge.Factory.defaultInstance();

        externalBridgeInterface = new ExternalBridgeInterfaceImpl(mCSEventEmitterModuleImpl, ExternalBridgeType.REACT_NATIVE);
        xpfInterfaceBridge.registerExternalBridge(externalBridgeInterface);

        // type-based masking
        Contentsquare.mask(com.contentsquare.rn.masking.CSMaskedView.class);
        Contentsquare.unMask(com.contentsquare.rn.masking.CSUnmaskedView.class);
    }

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

    @ReactMethod
    public void start() {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            android.content.Context appContext = mReactContext.getApplicationContext();
            try {
                Contentsquare.start(appContext);
            } catch (Exception e) {
                Log.e("CSLIB START", "Error during Contentsquare.start(): " + e.getMessage(), e);
            }
        });
    }

    @ReactMethod
    public void send(String screenView, ReadableArray customVars) {
        if (customVars == null) {
            mReactNativeUiThreadUtil.runOnUiThread(() -> {
                Contentsquare.send(screenView);
            });
            return;
        }
        List<CustomVar> customVarList = new ArrayList<>();

        for (int i = 0; i < customVars.size(); i++) {
            ReadableType type = customVars.getType(i);
            if (type == ReadableType.Map) {
                convertAndAddToCustomVarList(customVars.getMap(i), customVarList);
            } else {
                Log.i("CSLIB", "The provided Custom Var does not match the expected format type.");
            }
        }
        CustomVar[] customVarsArray = customVarList.toArray(new CustomVar[0]);
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.send(screenView, customVarsArray);
        });
    }

    @ReactMethod
    public void optIn() {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.optIn(mReactContext);
        });
    }

    @ReactMethod
    public void optOut() {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.optOut(mReactContext);
        });
    }

    @ReactMethod
    public void setDefaultMasking(boolean isMasking) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.setDefaultMasking(isMasking);
        });
    }

    @ReactMethod
    public void sendTransaction(@Nullable String id, float value, int currency) {
        Transaction.TransactionBuilder tb = Transaction.Companion.builder(value, currency);
        if (id != null) {
            tb.id(id);
        }
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.send(tb.build());
        });
    }

    @ReactMethod
    public void sendTransactionWithStringCurrency(@Nullable String id, float value, @NonNull String currency) {
        Transaction.TransactionBuilder tb = Transaction.Companion.builder(value, currency);
        if (id != null) {
            tb.id(id);
        }
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.send(tb.build());
        });
    }

    @ReactMethod
    public void sendDynamicStringVar(@NonNull String key, @NonNull String value) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.send(key, value);
        });
    }

    @ReactMethod
    public void sendDynamicIntVar(@NonNull String key, int value) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.send(key, value);
        });
    }

    @ReactMethod
    public void stopTracking() {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.stopTracking();
        });
    }

    @ReactMethod
    public void resumeTracking() {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.resumeTracking();
        });
    }

    @ReactMethod
    public void getUserId(Callback cb) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            cb.invoke(Contentsquare.getUserId());
        });
    }

    @ReactMethod
    public void setOnSessionReplayLinkChange() {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.onSessionReplayLinkChange(sessionReplayLink -> {
                mCSEventEmitterModuleImpl.sendSessionReplayLink(sessionReplayLink);
            });
        });

    }

    @ReactMethod
    public void initComponents(@NonNull ReadableMap params) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            try {
                WritableMap updatedParams = Arguments.createMap();
                updatedParams.merge(params);

                if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
                    updatedParams.putBoolean("xpf_rn_new_arch_enabled",
                            BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
                }

                updatedParams.putBoolean("xpf_rn_csq_sdk_enabled",
                        false);

                // Get the TelemetryInterface class
                Class<?> telemetryInterfaceClass = Class
                        .forName("com.contentsquare.android.api.bridge.telemetry.TelemetryInterface");

                // Create an instance of the TelemetryInterface class
                Object telemetryInterfaceObj = telemetryInterfaceClass.newInstance();

                // Get the methods with reflection
                Method telemetryCollectMethod = telemetryInterfaceClass.getDeclaredMethod("telemetryCollect",
                        String.class, Object.class);
                Method setXPFTypeMethod = telemetryInterfaceClass.getDeclaredMethod("setXPFType", Object.class);
                Method setXPFVersionMethod = telemetryInterfaceClass.getDeclaredMethod("setXPFVersion", Object.class);
                Method setXPFBridgeVersionMethod = telemetryInterfaceClass.getDeclaredMethod("setXPFBridgeVersion",
                        Object.class);

                // Set methods accessible, as they are internal
                telemetryCollectMethod.setAccessible(true);
                setXPFTypeMethod.setAccessible(true);
                setXPFVersionMethod.setAccessible(true);
                setXPFBridgeVersionMethod.setAccessible(true);

                ReadableMapKeySetIterator iterator = updatedParams.keySetIterator();
                while (iterator.hasNextKey()) {
                    String key = iterator.nextKey();
                    switch (key) {
                        case "xpf_type":
                            try {
                                setXPFTypeMethod.invoke(telemetryInterfaceObj, updatedParams.getString(key));
                            } catch (Exception e) {
                                Log.d("CSLIB", "Exception failure while calling setXPFType", e);
                            }
                            break;

                        case "xpf_version":
                            try {
                                setXPFVersionMethod.invoke(telemetryInterfaceObj, updatedParams.getString(key));
                            } catch (Exception e) {
                                Log.d("CSLIB", "Exception failure while calling setXPFVersion", e);
                            }
                            break;

                        case "xpf_bridge_version":
                            try {
                                setXPFBridgeVersionMethod.invoke(telemetryInterfaceObj, updatedParams.getString(key));
                            } catch (Exception e) {
                                Log.d("CSLIB", "Exception failure while calling setXPFBridgeVersion", e);
                            }
                            break;

                        default:
                            try {
                                telemetryCollectMethod.invoke(telemetryInterfaceObj, key, getValueForKey(updatedParams, key));
                            } catch (Exception e) {
                                Log.d("CSLIB", "Exception failure while calling telemetryCollect", e);
                            }
                            break;
                    }
                }
            } catch (Exception e) {
                Log.e("CSLIB", "Exception failure while initializing components", e);
                e.printStackTrace();
            }
        });

    }

    /*
     * It will inject the CS JSInterface which will track and handle all the user
     * interaction events on this WebView.
     *
     * @param webViewTag the webView's react tag.
     */
    @ReactMethod
    public void injectWebView(final int webViewTag) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            mWebViewInjector.injectWebView(mReactContext, webViewTag);
        });
    }

    /**
     * It will remove the previously injected CS JSInterface. If there was no
     * interface
     * <p>
     * <p>
     * * @param webViewTag the webView's react tag.
     */
    @ReactMethod
    public void removeWebViewInjection(final int webViewTag) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            mWebViewInjector.removeWebViewInjection(mReactContext, webViewTag);
        });
    }

    @ReactMethod
    public void sendUserIdentifier(@NonNull final String userIdentifier) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            Contentsquare.sendUserIdentifier(userIdentifier);
        });
    }

    public void monitorWarn(@NonNull ReadableMap params) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            LogMonitorUtil.monitorWarn(params);
        });
    }

    public static Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();

        for (Map.Entry entry : Constants.currencies.entrySet()) {
            constants.put((String) entry.getKey(), entry.getValue());
        }
        return constants;
    }

    public void monitorError(@NonNull ReadableMap params) {
        mReactNativeUiThreadUtil.runOnUiThread(() -> {
            LogMonitorUtil.monitorError(params);
        });
    }
}
