package com.sumsub.msdk.plugins.reactnative;

import android.util.Log;

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

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
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.bridge.WritableNativeMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.sumsub.log.logger.Logger;
import com.sumsub.sns.core.SNSMobileSDK;
import com.sumsub.sns.core.SNSModule;
import com.sumsub.sns.core.SNSProoface;
import com.sumsub.sns.core.data.listener.SNSCompleteHandler;
import com.sumsub.sns.core.data.listener.SNSErrorHandler;
import com.sumsub.sns.core.data.listener.SNSEvent;
import com.sumsub.sns.core.data.listener.SNSEventHandler;
import com.sumsub.sns.core.data.listener.SNSStateChangedHandler;
import com.sumsub.sns.core.data.listener.TokenExpirationHandler;
import com.sumsub.sns.core.data.model.SNSCompletionResult;
import com.sumsub.sns.core.data.model.SNSDocumentDefinition;
import com.sumsub.sns.core.data.model.SNSException;
import com.sumsub.sns.core.data.model.SNSInitConfig;
import com.sumsub.sns.core.data.model.SNSSDKState;
import com.sumsub.sns.core.theme.SNSCustomizationFileFormat;

import org.jetbrains.annotations.NotNull;

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

public class SNSMobileSdkReactNativeModule extends ReactContextBaseJavaModule {

    private final ReactApplicationContext reactContext;
    private volatile static String newAccessToken;
    @Nullable
    private volatile static SNSMobileSDK.SDK sdk;

    private final static String LOG_TAG = "SNSRNModule";

    public SNSMobileSdkReactNativeModule(@NonNull ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    @ReactMethod
    public void updateAccessToken(String accessToken) {
        Log.d(LOG_TAG, "Sumsub: updateAccessToken " + accessToken + " " + this);
        newAccessToken = accessToken;
    }

    private void sendEvent(ReactContext reactContext,
                           String eventName,
                           @Nullable WritableMap params) {
        Log.d(LOG_TAG, "Sending event " + eventName);
        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
    }

    @ReactMethod
    public void launch(
            final String apiUrl,
            final String accessToken,
            final String locale,
            final boolean isDebug,
            final ReadableMap hasHandlers,
            final ReadableMap appConf,
            final ReadableMap preferredDocumentDefinitions,
            final ReadableMap theme,
            final ReadableMap settings,
            final ReadableMap strings,
            final boolean isAnalyticsEnabled,
            final Double autoCloseOnApprove,
            final Promise promise) {

        reactContext.runOnUiQueueThread(new Runnable() {
            @Override
            public void run() {
                try {
                    final SNSErrorHandler errorHandler = new SNSErrorHandler() {
                        @Override
                        public void onError(@NotNull SNSException e) {
                            Log.d(LOG_TAG, Log.getStackTraceString(e));
                        }
                    };

                    final SNSStateChangedHandler stateChangedHandler = new SNSStateChangedHandler() {
                        @Override
                        public void onStateChanged(@NotNull SNSSDKState oldState, @NotNull SNSSDKState newState) {
                            WritableMap map = Arguments.createMap();
                            map.putString("prevStatus", oldState.getClass().getSimpleName());
                            map.putString("newStatus", newState.getClass().getSimpleName());
                            sendEvent(reactContext, "onStatusChanged", map);
                        }
                    };

                    final SNSCompleteHandler completeHandler = new SNSCompleteHandler() {
                        @Override
                        public void onComplete(@NotNull SNSCompletionResult snsCompletionResult, @NotNull SNSSDKState snssdkState) {
                            getResultToTheClient(snsCompletionResult, snssdkState, promise);
                        }
                    };

                    final SNSEventHandler eventHandler = new SNSEventHandler() {
                        @Override
                        public void onEvent(@NotNull SNSEvent snsEvent) {
                            WritableMap params = Arguments.createMap();
                            final Map<String, Object> payload = snsEvent.getPayload();
                            for (String key : payload.keySet()) {
                                if (key.equals("isCanceled") || key.equals("isCancelled")) {
                                    params.putBoolean("isCancelled", (Boolean) payload.get(key));
                                } else {
                                    params.putString(key, payload.get(key).toString());
                                }
                            }
                            WritableMap map = Arguments.createMap();
                            map.putString("eventType", upperCaseFirstLetter(snsEvent.getEventType()));
                            map.putMap("payload", params);
                            sendEvent(reactContext, "onEvent", map);
                        }
                    };

                    TokenExpirationHandler tokenExpirationHandler = new TokenExpirationHandler() {
                        @Override
                        public String onTokenExpired() {
                            Log.d(LOG_TAG, "Calling onTokenExpired()! " + this + " @ " + Thread.currentThread().getName());
                            newAccessToken = null;

                            sendEvent(reactContext, "onTokenExpired", null);

                            Log.d(LOG_TAG, "onTokenExpired JS callback invoked. Waiting for new token...");
                            int maxWaitTime = 120_000;
                            int checkDelay = 100;
                            int timeSpentOnWaiting = 0;
                            while (newAccessToken == null) {
                                try {
                                    if (timeSpentOnWaiting % 1000 == 0) {
                                        Log.d(LOG_TAG, "Waiting for token... " + timeSpentOnWaiting + "ms @ " + Thread.currentThread().getName());
                                    }
                                    Thread.sleep(checkDelay);
                                    timeSpentOnWaiting += checkDelay;
                                } catch (InterruptedException e) {
                                    Log.e(LOG_TAG, "Interrupted while waiting for new access token", e);
                                    return newAccessToken;
                                }
                                if (timeSpentOnWaiting >= maxWaitTime) {
                                    Log.d(LOG_TAG, "Timeout while waiting for new access token. Waiting time: " + timeSpentOnWaiting + "ms");
                                    return newAccessToken;
                                }
                            }
                            Log.d(LOG_TAG, "Sumsub: Received new token: " + newAccessToken + ' ' + Thread.currentThread().getName());
                            return newAccessToken;
                        }
                    };

                    SNSMobileSDK.Builder builder;

                    builder = new SNSMobileSDK.Builder(getCurrentActivity());

                    if (apiUrl != null && !apiUrl.isEmpty()) {
                        builder.withBaseUrl(apiUrl);
                    }

                    builder
                            .withAccessToken(accessToken, tokenExpirationHandler)
                            .withDebug(isDebug)
                            .withLocale(new Locale(locale == null ? Locale.getDefault().getLanguage() : locale))
                            .withCompleteHandler(completeHandler)
                            .withStateChangedHandler(stateChangedHandler)
                            .withSettings(toStringMap(settings))
                            .withErrorHandler(errorHandler)
                            .withEventHandler(eventHandler)
                            .withAnalyticsEnabled(isAnalyticsEnabled)
                            .withLogTree(new RnModuleLogger(reactContext));

                    String email = appConf.hasKey("email") ? appConf.getString("email") : null;
                    String phone = appConf.hasKey("phone") ? appConf.getString("phone") : null;

                    builder.withConf(new SNSInitConfig(email, phone, toStringMap(strings)));

                    if (preferredDocumentDefinitions != null) {
                        Iterator<Map.Entry<String, Object>> iterator = preferredDocumentDefinitions.getEntryIterator();
                        HashMap<String, SNSDocumentDefinition> documents = new HashMap<>();
                        while (iterator.hasNext()) {
                            try {
                                Map.Entry<String, Object> entry = iterator.next();
                                String key = entry.getKey();
                                ReadableMap value = (ReadableMap) entry.getValue();
                                String idDocType = value.getString("idDocType");
                                String country = value.getString("country");
                                documents.put(key, new SNSDocumentDefinition(idDocType, country));
                            } catch (Exception e) {
                                Log.e(LOG_TAG, e.toString());
                            }
                        }
                        if (!documents.isEmpty()) {
                            builder.withPreferredDocumentDefinitions(documents);
                        }
                    }

                    if (theme != null) {
                        SNSMobileSDK.INSTANCE.isDebug();
                        builder.withMappedTheme(readableMapToHashMap(theme), SNSCustomizationFileFormat.REACT_NATIVE);
                    }

                    List<SNSModule> modules = new ArrayList<>();
                    modules.add(new SNSProoface());
                    builder.withModules(modules);

                    builder.withAutoCloseOnApprove(autoCloseOnApprove.intValue());
                    sdk = builder.build();
                    sdk.launch();
                } catch (Exception e) {
                    Log.e(LOG_TAG, e.toString());
                    promise.reject(e);
                }
            }
        });

    }

    @ReactMethod
    public void dismiss() {
        reactContext.runOnUiQueueThread(new Runnable() {
            @Override
            public void run() {
                if (sdk != null) sdk.dismiss();
            }
        });
    }

    private String upperCaseFirstLetter(String s) {
        return s.substring(0, 1).toUpperCase() + s.substring(1);
    }

    private void getResultToTheClient(SNSCompletionResult snsCompletionResult, SNSSDKState snssdkState, Promise promise) {
        if (snsCompletionResult instanceof SNSCompletionResult.SuccessTermination) {
            Log.d(LOG_TAG, "Resolving promise");
            promise.resolve(getResult(true, snssdkState, null, null));
        } else if (snsCompletionResult instanceof SNSCompletionResult.AbnormalTermination) {
            Log.d(LOG_TAG, "Rejecting promise");
            SNSCompletionResult.AbnormalTermination abnormalTermination = (SNSCompletionResult.AbnormalTermination) snsCompletionResult;
            String message = abnormalTermination.getException() != null ? abnormalTermination.getException().getMessage() : null;
            if (snssdkState instanceof SNSSDKState.Failed) {
                promise.resolve(getResult(false, snssdkState, message, snssdkState.getClass().getSimpleName()));
            } else {
                promise.resolve(getResult(false, new SNSSDKState.Failed.Unknown(new Exception()), message, "Unknown"));

            }
        } else {
            promise.reject("Unknown completion result: " + snsCompletionResult.getClass().getName());
        }
    }

    public static HashMap<String, Object> readableMapToHashMap(ReadableMap readableMap) {
        if (readableMap == null) {
            return null;
        }
        HashMap map = new HashMap<String, Object>();
        com.facebook.react.bridge.ReadableMapKeySetIterator keySetIterator = readableMap.keySetIterator();
        while (keySetIterator.hasNextKey()) {
            String key = keySetIterator.nextKey();
            com.facebook.react.bridge.ReadableType type = readableMap.getType(key);
            switch (type) {
                case String:
                    map.put(key, readableMap.getString(key));
                    break;
                case Number:
                    map.put(key, readableMap.getDouble(key));
                    break;
                case Array:
                    ReadableArray array = readableMap.getArray(key);
                    List<Map<String, Object>> result = new ArrayList<>();
                    int size = array != null ? array.size() : 0;
                    for (int i = 0; i < size; i++) {
                        result.add(readableMapToHashMap(array.getMap(i)));
                    }
                    map.put(key, result);
                    break;
                case Map:
                    HashMap<String, Object> attributes = readableMapToHashMap(readableMap.getMap(key));
                    map.put(key, attributes);
                    break;
                default:
                    // do nothing
            }
        }
        return map;
    }

    private Map<String, String> toStringMap(@Nullable ReadableMap readableMap) {
        Map<String, String> result = new HashMap<>();
        if (readableMap == null)
            return result;

        com.facebook.react.bridge.ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
        while (iterator.hasNextKey()) {
            String key = iterator.nextKey();
            result.put(key, readableMap.getString(key));
        }

        return result;
    }

    private WritableMap getResult(boolean success, SNSSDKState state, String errorMsg, String errorType) {
        final WritableMap result = new WritableNativeMap();
        result.putBoolean("success", success);
        result.putString("status", state != null ? state.getClass().getSimpleName() : "Unknown");
        result.putString("errorMsg", errorMsg);
        result.putString("errorType", errorType);
        return result;
    }


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

    private static class RnModuleLogger implements Logger {

        public RnModuleLogger(final ReactApplicationContext reactContext) {
            this.reactContext = reactContext;
        }

        private final ReactApplicationContext reactContext;

        @Override
        public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
            WritableMap map = Arguments.createMap();
            map.putString("message", message);
            reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onLog", map);
        }

        @Override
        public void w(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
            WritableMap map = Arguments.createMap();
            map.putString("message", message);
            reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onLog", map);
        }

        @Override
        public void d(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
            WritableMap map = Arguments.createMap();
            map.putString("message", message);
            reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onLog", map);
        }

        @Override
        public void i(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
            WritableMap map = Arguments.createMap();
            map.putString("message", message);
            reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onLog", map);
        }

        @Override
        public void v(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
            WritableMap map = Arguments.createMap();
            map.putString("message", message);
            reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onLog", map);
        }
    }
}
