package pendo.io.reactnative;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;

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

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.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.util.ReactFindViewUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import sdk.pendo.io.Pendo;
import sdk.pendo.io.sdk.react.PlatformStateManager;

public class ReactNativePendoModule extends ReactContextBaseJavaModule {

    private static final String TAG = "ReactNativePendoModule";
    private static final String NAME = "ReactNativePendo";
    private static final String VISITOR_ID = "visitorId";
    private static final String ACCOUNT_ID = "accountId";
    private static final String VISITOR_DATA = "visitorData";
    private static final String ACCOUNT_DATA = "accountData";
    private static final String OPTIONS = "options";
    private static final String ENVIRONMENT_NAME = "environmentName";
    private static final String DEBUG_MODE = "debugMode";
    private static final String REACT_NATIVE_VERSION = "reactNativeVersion";

    private static final String PLUGIN_VERSION = "pluginVersion";
    private static final String USE_CLICKABLE_ELEMENTS_FROM_JS = "useClickableElementsFromJS";
    private static final String ERROR_MESSAGE = "errorMessage";
    private static final String SPACING = " - ";
    private static final String ON_SCREEN_CONTENT_CHANGED = "onScreenContentChange";
    private static final String ON_MODAL_STATE_VISIBLE = "onModalStateVisible";
    private static final String ON_MODAL_STATE_HIDDEN = "onModalStateHidden";

    private static final int REACT_NATIVE_NAVIGATION = 1;
    private static final int REACT_NAVIGATION = 2;
    private static final int EXPO_ROUTER = 5;

    private boolean isDebugModeEnabled = false;



    private enum LogLevel {
        DEBUG;
    }

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

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

    public static List<Integer> toIntList(@Nullable ReadableArray readableArray) {
        List<Integer> result = new ArrayList<>();
        if (readableArray == null || readableArray.size() == 0) {
            return result;
        }
        try {
            List<Object> list = readableArray.toArrayList();
            for (int index = 0; index < list.size(); index++) {
                Object value = list.get(index);
                if (value instanceof Integer) {
                    result.add((Integer) value);
                } else if (value instanceof Double) {
                    result.add(((Double) value).intValue());
                } else if (value instanceof Float) {
                    result.add(((Float) value).intValue());
                }
            }
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("toIntList", e), false);
        }
        return result;
    }


    /**
     * toMap converts a {@link ReadableMap} into a HashMap<String, String>.
     *
     * @param readableMap The ReadableMap to be converted.
     * @return A HashMap containing the data that was in the ReadableMap.
     */
    private static Map<String, Object> toMap(@Nullable ReadableMap readableMap) {
        Map<String, Object> map = new HashMap<>();
        if (readableMap == null) {
            return map;
        }
        try {
            map = readableMap.toHashMap();
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("toMap", e), false);
        }
        return map;
    }

    private static boolean isNullOrWhiteSpace(String value) {
        return value == null || value.trim().isEmpty();
    }

    private static Map<String, Object> createErrorMessageMap(String errorSendFromMethodNamed, Exception e) {
        Map<String, Object> errorMap = new HashMap<>();
        if (e != null) {
            errorMap.put(ERROR_MESSAGE, errorSendFromMethodNamed + SPACING + e.getMessage());
        }
        return errorMap;
    }

    /**
     * Send indication to the native SDK when something went wrong in the RN plugin
     *
     * @param userInfo            a map to pass in the relevant data (i.e error message).
     * @param shouldSendErrorToBE a boolean indicating whether the error should be sent to the backend.
     */
    private static void sendFailureInfo(Map<String, Object> userInfo, boolean shouldSendErrorToBE) {
        PlatformStateManager.INSTANCE.sendFailureInfo(userInfo);
    }

    /*
        Errors sent from JS part of the plugin
     */
    @ReactMethod
    public void sendFailureInfo(ReadableMap userInfo, boolean shouldSendErrorToBE) {
        sendFailureInfo(toMap(userInfo), shouldSendErrorToBE);
    }

    @ReactMethod
    public void setup(@NonNull String appKey, int navigationLibrary, @Nullable ReadableMap options) {
        Context appContext = getReactApplicationContext().getCurrentActivity();
        //Fallback
        if(appContext == null) {
            appContext = getReactApplicationContext();
        }
        if (appContext != null) {
            Map<String, Object> additionalOptions = new HashMap();
            additionalOptions.put(Pendo.PendoOptions.FRAMEWORK, Pendo.PendoOptions.Framework.REACT_NATIVE);
            switch (navigationLibrary) {
                case REACT_NATIVE_NAVIGATION:
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_TYPE, Pendo.PendoOptions.FrameworkType.REACT_NATIVE_NAVIGATION);
                    break;
                case REACT_NAVIGATION:
                case EXPO_ROUTER:
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_TYPE, Pendo.PendoOptions.FrameworkType.REACT_NAVIGATION);
                    break;
                default:
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_TYPE, Pendo.PendoOptions.FrameworkType.TRACK);
            }
            Pendo.PendoOptions.Builder pendoOptionsBuilder = new Pendo.PendoOptions.Builder();
            if (options != null) {
                Map<String, Object> optionsMap = toMap(options);
                if (optionsMap.containsKey(ENVIRONMENT_NAME)) {
                    pendoOptionsBuilder.setEnvironmentName((String) optionsMap.get(ENVIRONMENT_NAME));
                }
                if (optionsMap.containsKey(USE_CLICKABLE_ELEMENTS_FROM_JS)) {
                    pendoOptionsBuilder.setUseClickableElementsFromJS((Boolean) optionsMap.get(USE_CLICKABLE_ELEMENTS_FROM_JS));
                }
                if (optionsMap.containsKey(REACT_NATIVE_VERSION)) {
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_VERSION, (String) optionsMap.get(REACT_NATIVE_VERSION));
                }

                if(optionsMap.containsKey(PLUGIN_VERSION)) {
                    additionalOptions.put(PLUGIN_VERSION, (String) optionsMap.get(PLUGIN_VERSION));
                }
            }
            pendoOptionsBuilder.setAdditionalOptions(additionalOptions);
            Pendo.setup(appContext, appKey, pendoOptionsBuilder.build(), null);
        } else {
            printDebugLog(TAG, "Cannot call setup() due to application context being null", LogLevel.DEBUG);
        }
    }

    /**
     * Ends current session, resets all managers and listeners, and does not start a new session
     * until switchVisitor is called
     */
    @ReactMethod
    public void endSession() {
        Pendo.endSession();
    }

    /**
     * Ends current session, resets all managers and listeners, and starts a new session
     * with the new parameters.
     *
     * @param visitorId
     * @param accountId
     * @param visitorData
     * @param accountData
     */
    @ReactMethod
    public void startSession(String visitorId, String accountId, @Nullable ReadableMap visitorData, @Nullable ReadableMap accountData) {
        Pendo.startSession(visitorId, accountId, toMap(visitorData), toMap(accountData));
    }

    @ReactMethod
    public void setVisitorData(@Nullable ReadableMap visitorData) {
        Pendo.setVisitorData(toMap(visitorData));
    }

    @ReactMethod
    public void setAccountData(@Nullable ReadableMap accountData) {
        Pendo.setAccountData(toMap(accountData));
    }

    /**
     * Track an event that has happened in your application.
     *
     * @param event      - the name of the event.
     * @param properties - properties connected to the event.
     */
    @ReactMethod
    public void track(@NonNull String event, @Nullable ReadableMap properties) {
        if (isNullOrWhiteSpace(event)) {
            throw new IllegalArgumentException("Event name is required");
        }
        Pendo.track(event, toMap(properties));
    }

    /**
     * Pause showing guides during this session
     */
    @ReactMethod
    public void pauseGuides(boolean dismissGuides) {
        Pendo.pauseGuides(dismissGuides);
    }

    /**
     * Resume showing guides during this session
     */
    @ReactMethod
    public void resumeGuides() {
        Pendo.resumeGuides();
    }

    /**
     * Dismiss all visible guides
     */
    @ReactMethod
    public void dismissVisibleGuides() {
        Pendo.dismissVisibleGuides();
    }

    /**
     * Get visitor Id
     */
    @ReactMethod
    public void getVisitorId(Promise promise) {
        try {
            promise.resolve(Pendo.getVisitorId());
        } catch (Exception e) {
            promise.reject(e);
        }
    }

    /**
     * Get account Id
     */
    @ReactMethod
    public void getAccountId(Promise promise) {
        try {
            promise.resolve(Pendo.getAccountId());
        } catch (Exception e) {
            promise.reject(e);
        }
    }

    /**
     * Get device Id
     */
    @ReactMethod
    public void getDeviceId(Promise promise) {
        try {
            promise.resolve(Pendo.getDeviceId());
        } catch (Exception e) {
            promise.reject(e);
        }
    }

    /**
     * Call this to let the native side know a screen has changed in the JS.
     *
     * @param screenName - the name of the new screen.
     * @param rootTags   - the list of root react tags
     * @param info       - information about the screen including type of structure, clickable elements etc...
     */
    @ReactMethod
    public void screenChanged(String screenName, ReadableArray rootTags, ReadableArray clickableElements, ReadableMap info) {
        try {
            if (screenName != null && rootTags.size() > 0) {
                printDebugLog(TAG, "screenChanged, screenName: " + screenName + " rootTag: " + rootTags.toString() +
                        " clickableElements: " + clickableElements.toString() + " info: " + info, LogLevel.DEBUG);
                PlatformStateManager.INSTANCE.newScreenIdentified(screenName, toIntList(rootTags), toMap(info), clickableElements.toArrayList());
            } else {
                printDebugLog(TAG, "screenChanged, either screenName is null or rootTag <= 0", LogLevel.DEBUG);
            }
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("screenChanged", e), false);
        }
    }

    /**
     * Enable/disable debug logs
     */
    @ReactMethod
    public void setDebugMode(boolean isDebugEnabled) {
        isDebugModeEnabled = isDebugEnabled;
        Pendo.setDebugMode(isDebugEnabled);
    }

    /**
     * Finding view with provided nativeID and ivokes native sendClickAnalytic API
     */
    @ReactMethod
    public void sendClickAnalytic(String nativeID) {
        Activity activity = getCurrentActivity();
        if (activity != null) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    View rootView = activity.getWindow().getDecorView().getRootView();
                    View requiredView = ReactFindViewUtil.findView(rootView, nativeID);
                    Pendo.sendClickAnalytic(requiredView);
                }
            });
        } else {
            printDebugLog(TAG, "Cannot call sendClickAnalytic() due to activity being null", LogLevel.DEBUG);
        }
    }

    /**
     * This method wraps usage of the Log class depend on the isDebugModeEnabled flag
     *
     * @param tag      to define for log message
     * @param message  to print
     * @param logLevel to define for log message
     */
    private void printDebugLog(String tag, String message, LogLevel logLevel) {
        if (!isDebugModeEnabled) {
            return;
        }
        switch (logLevel) {
            case DEBUG:
                Log.d(tag, message);
                break;
            default:
                Log.d(tag, message);
        }
    }

    /**
     * Stub method that implemented for iOS
     */
    @ReactMethod
    public void shouldScanForDynamicElements(Boolean scanDynamicElements) {
    }

    /**
     * Trigger a screenContent scan to manually detect changes on screen
     */
    @ReactMethod
    public void screenContentChanged() {
        this.sendEvent(ON_SCREEN_CONTENT_CHANGED, null);
    }

    /**
     * Trigger a modalStateChange scan to automatically detect changes on screen when modal is shown or hidden
     */
    @ReactMethod
    public void modalStateChanged(boolean isVisible) {
        if (isVisible) {
            this.sendEvent(ON_MODAL_STATE_VISIBLE, null);
        } else {
            this.sendEvent(ON_MODAL_STATE_HIDDEN, null);
        }
    }

    private void sendEvent(String eventName, @Nullable WritableMap params) {
        ReactApplicationContext reactContext = this.getReactApplicationContext();
        try {
            reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("sendEvent", e), false);
        }
    }

    // Required for rn built in EventEmitter Calls.
    @ReactMethod
    public void addListener(String eventName) {
    }

    @ReactMethod
    public void removeListeners(Integer count) {
    }
}
