package com.withpersona.sdk2.reactnative;

import android.app.Activity;
import android.content.Intent;

import androidx.annotation.Nullable;

import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
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.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.withpersona.sdk2.reactnative.R;
import com.withpersona.sdk2.inquiry.ClientThemeSource;
import com.withpersona.sdk2.inquiry.Environment;
import com.withpersona.sdk2.inquiry.Fields;
import com.withpersona.sdk2.inquiry.Inquiry;
import com.withpersona.sdk2.inquiry.InquiryBuilder;
import com.withpersona.sdk2.inquiry.InquiryField;
import com.withpersona.sdk2.inquiry.InquiryResponse;
import com.withpersona.sdk2.inquiry.InquiryTemplateBuilder;
import com.withpersona.sdk2.inquiry.ServerThemeSource;
import com.withpersona.sdk2.inquiry.types.collected_data.CollectedData;
import com.withpersona.sdk2.inquiry.types.collected_data.DocumentFile;
import com.withpersona.sdk2.inquiry.types.collected_data.GovernmentIdCapture;
import com.withpersona.sdk2.inquiry.types.collected_data.SelfieCapture;
import com.withpersona.sdk2.inquiry.types.collected_data.StepData;

import org.jetbrains.annotations.NotNull;

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

public class PersonaInquiryModule2 extends ReactContextBaseJavaModule
    implements ActivityEventListener {

  private static final int PERSONA_INQUIRY_REQUEST_CODE = 31416;
  private static final String INQUIRY_ID = "inquiryId";
  private static final String TEMPLATE_ID = "templateId";
  private static final String TEMPLATE_VERSION = "templateVersion";
  private static final String ACCOUNT_ID = "accountId";
  private static final String REFERENCE_ID = "referenceId";
  private static final String ACCESS_TOKEN = "sessionToken";
  private static final String ENVIRONMENT = "environment";
  private static final String ENVIRONMENT_ID = "environmentId";
  private static final String FIELDS = "fields";
  private static final String FIELD_ADDITIONAL_FIELDS = "additionalFields";
  private static final String RETURN_COLLECTED_DATA = "returnCollectedData";
  private static final String THEME_SOURCE = "themeSource";
  private static final String THEME_SET_ID = "themeSetId";
  private static final String LOCALE = "locale";

  private final ReactApplicationContext reactContext;

  public PersonaInquiryModule2(ReactApplicationContext reactContext) {
    super(reactContext);
    this.reactContext = reactContext;
    reactContext.addActivityEventListener(this);
  }

  @Override
  public void onNewIntent(Intent intent) {
    // Do nothing
  }

  @Override
  public void onActivityResult(
      final Activity activity,
      final int requestCode,
      final int resultCode,
      @Nullable Intent data) {
    if (requestCode == PERSONA_INQUIRY_REQUEST_CODE) {
      InquiryResponse response = Inquiry.onActivityResult(data);
      DeviceEventManagerModule.RCTDeviceEventEmitter jsModule =
          this.reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);

      if (response instanceof InquiryResponse.Complete) {
        InquiryResponse.Complete complete = ((InquiryResponse.Complete) response);

        // Build event params
        WritableMap params = Arguments.createMap();
        params.putString("inquiryId", complete.getInquiryId());
        params.putString("status", complete.getStatus());

        WritableMap fields = Arguments.createMap();
        Map<String, InquiryField> rawFields = complete.getFields();
        for (String fieldName : rawFields.keySet()) {
          InquiryField rawField = rawFields.get(fieldName);
          if (rawField instanceof InquiryField.StringField) {
            fields.putMap(fieldName,
                wrapField("string", ((InquiryField.StringField) rawField).getValue()));
          } else if (rawField instanceof InquiryField.IntegerField) {
            Integer value = ((InquiryField.IntegerField) rawField).getValue();
            fields.putMap(fieldName,
                wrapField("integer", (value == null) ? null : value.toString()));
          } else if (rawField instanceof InquiryField.BooleanField) {
            Boolean value = ((InquiryField.BooleanField) rawField).getValue();
            fields.putMap(fieldName,
                wrapField("boolean", (value == null) ? null : value.toString()));
          } else if (rawField instanceof InquiryField.UnknownField) {
            String type = ((InquiryField.UnknownField) rawField).getType();
            fields.putMap(fieldName, wrapField("unknown", type));
          } else {
            fields.putMap(fieldName, wrapField("unknown", null));
          }
        }
        params.putMap("fields", fields);

        params.putMap("collectedData", collectedDataToMap(complete.getCollectedData()));

        jsModule.emit("onComplete", params);
      } else if (response instanceof InquiryResponse.Cancel) {
        InquiryResponse.Cancel cancel = (InquiryResponse.Cancel) response;

        // Build event params
        WritableMap params = Arguments.createMap();
        params.putString("inquiryId", cancel.getInquiryId());
        params.putString("sessionToken", cancel.getSessionToken());
        jsModule.emit("onCanceled", params);
      } else if (response instanceof InquiryResponse.Error) {
        InquiryResponse.Error error = (InquiryResponse.Error) response;

        WritableMap params = Arguments.createMap();
        params.putString("debugMessage", error.getDebugMessage());
        params.putString("errorCode", error.getErrorCode().name());
        jsModule.emit("onError", params);
      }
    }
  }

  @Nullable
  private ReadableMap collectedDataToMap(@Nullable CollectedData collectedData) {
    if (collectedData == null) {
      return null;
    }

    WritableMap collectedDataMap = Arguments.createMap();

    WritableArray stepDataMap = Arguments.createArray();
    for (StepData stepDatum : collectedData.getStepData()) {
      WritableMap stepDatumMap = Arguments.createMap();
      stepDatumMap.putString("stepName", stepDatum.getStepName());

      if (stepDatum instanceof StepData.DocumentStepData) {
        WritableArray documentsArr = Arguments.createArray();

        for (DocumentFile document : ((StepData.DocumentStepData) stepDatum).getDocuments()) {
          WritableMap documentMap = Arguments.createMap();
          documentMap.putString("absoluteFilePath", document.getData().getAbsolutePath());
          documentsArr.pushMap(documentMap);
        }

        stepDatumMap.putArray("documents", documentsArr);
        stepDatumMap.putString("type", "DocumentStepData");
      } else if (stepDatum instanceof StepData.GovernmentIdStepData) {
        WritableArray capturesArr = Arguments.createArray();

        for (GovernmentIdCapture capture : ((StepData.GovernmentIdStepData) stepDatum).getCaptures()) {
          WritableMap captureMap = Arguments.createMap();
          captureMap.putString("idClass", capture.getIdClass());
          captureMap.putString("captureMethod", capture.getCaptureMethod().name());
          captureMap.putString("side", capture.getSide().name());

          WritableArray framesArr = Arguments.createArray();
          for (GovernmentIdCapture.Frame frame : capture.getFrames()) {
            WritableMap frameMap = Arguments.createMap();
            frameMap.putString("absoluteFilePath", frame.getData().getAbsolutePath());

            framesArr.pushMap(frameMap);
          }
          captureMap.putArray("frames", framesArr);

          capturesArr.pushMap(captureMap);
        }

        stepDatumMap.putArray("captures", capturesArr);
        stepDatumMap.putString("type", "GovernmentIdStepData");
      } else if (stepDatum instanceof StepData.SelfieStepData) {
        StepData.SelfieStepData selfieStepData = (StepData.SelfieStepData) stepDatum;
        ReadableMap centerCaptureMap = selfieCaptureToMap(selfieStepData.getCenterCapture());
        ReadableMap leftCaptureMap = selfieCaptureToMap(selfieStepData.getLeftCapture());
        ReadableMap rightCaptureMap = selfieCaptureToMap(selfieStepData.getRightCapture());

        stepDatumMap.putMap("centerCapture", centerCaptureMap);
        stepDatumMap.putMap("leftCapture", leftCaptureMap);
        stepDatumMap.putMap("rightCapture", rightCaptureMap);
        stepDatumMap.putString("type", "SelfieStepData");
      } else if (stepDatum instanceof StepData.UiStepData) {
        Map<String, Object> componentParams = ((StepData.UiStepData) stepDatum).getComponentParams();

        stepDatumMap.putMap("componentParams", uiStepParamsMapToMap(componentParams));
        stepDatumMap.putString("type", "UiStepData");
      } else {
        // do nothing
      }

      stepDataMap.pushMap(stepDatumMap);
    }

    collectedDataMap.putArray("stepData", stepDataMap);

    return collectedDataMap;
  }

  private ReadableMap uiStepParamsMapToMap(Map<?, ?> map) {
    WritableMap resultMap = Arguments.createMap();
    for (Map.Entry<?, ?> param : map.entrySet()) {
      Object key = param.getKey();
      Object value = param.getValue();

      if (!(key instanceof String)) {
        continue; // All ui step params should have string keys.
      }

      String keyStr = (String) key;

      if (value instanceof Map<?, ?>) {
        resultMap.putMap(keyStr, uiStepParamsMapToMap((Map<?, ?>) value));
      } else if (value instanceof List<?>) {
        resultMap.putArray(keyStr, uiStepParamsArrToArr((List<?>) value));
      } else if (value instanceof String) {
        resultMap.putString(keyStr, (String) value);
      } else if (value instanceof Boolean) {
        resultMap.putBoolean(keyStr, (Boolean) value);
      } else if (value instanceof Number) {
        resultMap.putDouble(keyStr, (Double) value);
      }
    }

    return resultMap;
  }

  private ReadableArray uiStepParamsArrToArr(List<?> arr) {
    WritableArray resultArr = Arguments.createArray();

    for (Object element : arr) {
      if (element instanceof String) {
        resultArr.pushString((String) element);
      } else if (element instanceof Boolean) {
        resultArr.pushBoolean((Boolean) element);
      } else if (element instanceof Number) {
        resultArr.pushDouble((Double) element);
      }
    }


    return resultArr;
  }

  private ReadableMap selfieCaptureToMap(@Nullable SelfieCapture selfieCapture) {
    if (selfieCapture == null) {
      return null;
    }

    WritableMap captureMap = Arguments.createMap();
    captureMap.putString("captureMethod", selfieCapture.getCaptureMethod().name());
    captureMap.putString("absoluteFilePath", selfieCapture.getData().getAbsolutePath());
    return captureMap;
  }


  private static ReadableMap wrapField(String type, String value) {
    WritableMap map = Arguments.createMap();
    map.putString("type", type);
    map.putString("value", value);
    return map;
  }

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

  /**
   * Launch an Inquiry Flow. In order for ease of implementation, this will
   * not behave defensively and will assume that proper arguments are sent over
   * the React Native bridge.
   */
  @ReactMethod
  @SuppressWarnings("unchecked")
  public void startInquiry(
      ReadableMap options
  ) {
    Activity currentActivity = reactContext.getCurrentActivity();

    String inquiryId = options.hasKey(INQUIRY_ID) ? options.getString(INQUIRY_ID) : null;
    String templateId = options.hasKey(TEMPLATE_ID) ? options.getString(TEMPLATE_ID) : null;
    String templateVersion = options.hasKey(TEMPLATE_VERSION) ? options.getString(TEMPLATE_VERSION) : null;

    if (inquiryId != null) {
      InquiryBuilder builder = Inquiry.fromInquiry(inquiryId);

      String themeSource = options.hasKey(THEME_SOURCE) ? options.getString(THEME_SOURCE) : null;
      builder = builder.theme(themeSource != null && themeSource.equals("server") ?
              new ServerThemeSource(R.style.Persona_Inquiry2_Theme) : new ClientThemeSource(R.style.Persona_Inquiry2_Theme));

      String sessionToken = options.hasKey(ACCESS_TOKEN) ? options.getString(ACCESS_TOKEN) : null;
      if (sessionToken != null) {
        builder = builder.sessionToken(sessionToken);
      }

      String locale = options.hasKey(LOCALE) ? options.getString(LOCALE) : null;
      if (locale != null) {
        builder = builder.locale(locale);
      }

      if (currentActivity != null) {
        builder.build().start(currentActivity, PERSONA_INQUIRY_REQUEST_CODE);
      }
    }

    if (templateId != null || templateVersion != null) {
      InquiryTemplateBuilder builder;
      if (templateId != null) {
        builder = Inquiry.fromTemplate(templateId);
      } else {
        builder = Inquiry.fromTemplateVersion(templateVersion);
      }

      String themeSource = options.hasKey(THEME_SOURCE) ? options.getString(THEME_SOURCE) : null;
      builder = builder.theme(themeSource != null && themeSource.equals("server") ?
              new ServerThemeSource(R.style.Persona_Inquiry2_Theme) : new ClientThemeSource(R.style.Persona_Inquiry2_Theme));

      String referenceId = options.hasKey(REFERENCE_ID) ? options.getString(REFERENCE_ID) : null;
      if (referenceId != null) {
        builder = builder.referenceId(referenceId);
      }

      String accountId = options.hasKey(ACCOUNT_ID) ? options.getString(ACCOUNT_ID) : null;
      if (accountId != null) {
        builder = builder.accountId(accountId);
      }

      Environment environment = environmentFromString(
          options.hasKey(ENVIRONMENT) ? options.getString(ENVIRONMENT) : null
      );
      if (environment != null) {
        builder = builder.environment(environment);
      }

      String environmentId = options.hasKey(ENVIRONMENT_ID) ? options.getString(ENVIRONMENT_ID) : null;
      if (environmentId != null) {
        builder = builder.environmentId(environmentId);
      }

      String themeSetId = options.hasKey(THEME_SET_ID) ? options.getString(THEME_SET_ID) : null;
      if (themeSetId != null) {
        builder = builder.themeSetId(themeSetId);
      }

      String locale = options.hasKey(LOCALE) ? options.getString(LOCALE) : null;
      if (locale != null) {
        builder = builder.locale(locale);
      }

      ReadableMap fields = options.hasKey(FIELDS) ? options.getMap(FIELDS) : null;

      if (fields != null) {
        Fields.Builder fieldsBuilder = new Fields.Builder();

        for (HashMap.Entry<String, Object> entry : fields.toHashMap().entrySet()) {
          String key = entry.getKey();
          Map<String, Object> wrappedValue = (Map<String, Object>) entry.getValue();
          String type = (String) wrappedValue.get("type");
          Object value = wrappedValue.get("value");

          if (value == null) {
            continue;
          }

          if (Objects.equals(type, "string")) {
            fieldsBuilder.field(key, (String) value);
          } else if (Objects.equals(type, "integer")) {
            fieldsBuilder.field(key, ((Double) value).intValue());
          } else if (Objects.equals(type, "boolean")) {
            fieldsBuilder.field(key, (Boolean) value);
          }
        }

        builder = builder.fields(fieldsBuilder.build());
      }

      Boolean returnCollectedData = options.hasKey(RETURN_COLLECTED_DATA) ? options.getBoolean(RETURN_COLLECTED_DATA) : null;
      if (returnCollectedData != null) {
        builder = builder.returnCollectedData(returnCollectedData);
      }

      if (currentActivity != null) {
        builder.build().start(currentActivity, PERSONA_INQUIRY_REQUEST_CODE);
      }
    }
  }

  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put("INQUIRY_SDK_VERSION", com.withpersona.sdk2.inquiry.BuildConfig.INQUIRY_SDK_VERSION);
    return constants;
  }

  private Environment environmentFromString(@Nullable String environment) {
    if (environment == null) {
      return null;
    }

    switch (environment) {
      case "production":
        return Environment.PRODUCTION;
      case "sandbox":
        return Environment.SANDBOX;
      default:
        return null;
    }
  }
}
