/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.cloudbridge

import android.content.Context
import com.facebook.FacebookSdk
import com.facebook.GraphRequest
import com.facebook.GraphResponse
import com.facebook.HttpMethod
import com.facebook.LoggingBehavior
import com.facebook.internal.Logger
import com.facebook.internal.Utility.convertJSONArrayToList
import com.facebook.internal.Utility.convertJSONObjectToHashMap
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.net.MalformedURLException
import java.net.URL
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

enum class SettingsAPIFields(val rawValue: String) {
  URL("endpoint"),
  ENABLED("is_enabled"),
  DATASETID("dataset_id"),
  ACCESSKEY("access_key")
}

object AppEventsCAPIManager {

  private const val SETTINGS_PATH = "/cloudbridge_settings"
  private val TAG = AppEventsCAPIManager::class.java.canonicalName
  internal var isEnabled: Boolean = false

  internal var savedCloudBridgeCredentials: Map<String, Any>?
    @JvmStatic
    @AutoHandleExceptions
    get() {
      val context = FacebookSdk.getApplicationContext()
      val sharedPref =
          context.getSharedPreferences(
              FacebookSdk.CLOUDBRIDGE_SAVED_CREDENTIALS, Context.MODE_PRIVATE)
              ?: return null

      val datasetID = sharedPref.getString(SettingsAPIFields.DATASETID.rawValue, null)
      val url = sharedPref.getString(SettingsAPIFields.URL.rawValue, null)
      val accessKey = sharedPref.getString(SettingsAPIFields.ACCESSKEY.rawValue, null)

      // individual cloud bridge settings should never be null
      if (datasetID.isNullOrBlank() || url.isNullOrBlank() || accessKey.isNullOrBlank()) {
        return null
      }

      val savedSettings: MutableMap<String, String> = mutableMapOf()
      savedSettings[SettingsAPIFields.URL.rawValue] = url
      savedSettings[SettingsAPIFields.DATASETID.rawValue] = datasetID
      savedSettings[SettingsAPIFields.ACCESSKEY.rawValue] = accessKey

      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG.toString(),
          " \n\nLoading Cloudbridge settings from saved Prefs: \n================\n DATASETID: %s\n URL: %s \n ACCESSKEY: %s \n\n ",
          datasetID,
          url,
          accessKey)

      return savedSettings
    }
    set(valuesToSave) {
      val context = FacebookSdk.getApplicationContext()
      val sharedPref =
          context.getSharedPreferences(
              FacebookSdk.CLOUDBRIDGE_SAVED_CREDENTIALS, Context.MODE_PRIVATE)
              ?: return

      if (valuesToSave == null) {
        val editor = sharedPref.edit()
        editor.clear()
        editor.apply()
        return
      }
      val datasetID = valuesToSave[SettingsAPIFields.DATASETID.rawValue]
      val url = valuesToSave[SettingsAPIFields.URL.rawValue]
      val accessKey = valuesToSave[SettingsAPIFields.ACCESSKEY.rawValue]

      // individual cloud bridge settings should never be null
      if (datasetID == null || url == null || accessKey == null) {
        return
      }

      val editor = sharedPref.edit()
      editor.putString(SettingsAPIFields.DATASETID.rawValue, datasetID.toString())
      editor.putString(SettingsAPIFields.URL.rawValue, url.toString())
      editor.putString(SettingsAPIFields.ACCESSKEY.rawValue, accessKey.toString())
      editor.apply()

      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG.toString(),
          " \n\nSaving Cloudbridge settings from saved Prefs: \n================\n DATASETID: %s\n URL: %s \n ACCESSKEY: %s \n\n ",
          datasetID,
          url,
          accessKey)
    }

  /** Run this to pull the CAPIG initialization settings from FB Backend. */
  @JvmStatic
  fun enable() {
    try {

      val callback =
          GraphRequest.Callback { response -> getCAPIGSettingsFromGraphResponse(response) }

      val graphReq =
          GraphRequest(
              null, FacebookSdk.getApplicationId() + SETTINGS_PATH, null, HttpMethod.GET, callback)

      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG as String,
          " \n\nCreating Graph Request: \n=============\n%s\n\n ",
          graphReq)

      graphReq.executeAsync()
    } catch (e: JSONException) {
      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG as String,
          " \n\nGraph Request Exception: \n=============\n%s\n\n ",
          e.stackTraceToString())
    }
  }

  internal fun getCAPIGSettingsFromGraphResponse(response: GraphResponse) {
    if (response.error != null) {

      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG as String,
          " \n\nGraph Response Error: \n================\nResponse Error: %s\nResponse Error Exception: %s\n\n ",
          response.error.toString(),
          response.error.exception.toString())

      val cbCredentials = savedCloudBridgeCredentials

      // load from cached behaviour
      if (cbCredentials != null) {

        val extractURL = URL(cbCredentials[SettingsAPIFields.URL.rawValue].toString())

        AppEventsConversionsAPITransformerWebRequests.configure(
            cbCredentials[SettingsAPIFields.DATASETID.rawValue].toString(),
            extractURL.protocol + "://" + extractURL.host,
            cbCredentials[SettingsAPIFields.ACCESSKEY.rawValue].toString())

        this.isEnabled = true
      }

      return
    }

    Logger.log(
        LoggingBehavior.APP_EVENTS,
        TAG as String,
        " \n\nGraph Response Received: \n================\n%s\n\n ",
        response)

    val result = response.getJSONObject()
    val url: String?
    val datasetID: String?
    val accessKey: String?
    val config: Map<String, Any>?
    try {
      val data = convertJSONArrayToList(result?.get("data") as JSONArray)
      config = convertJSONObjectToHashMap(JSONObject(data.firstOrNull()))

      url = config[SettingsAPIFields.URL.rawValue] as String?
      datasetID = config[SettingsAPIFields.DATASETID.rawValue] as String?
      accessKey = config[SettingsAPIFields.ACCESSKEY.rawValue] as String?
    } catch (e: JSONException) {
      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG,
          "CloudBridge Settings API response is not a valid json: \n%s ",
          e.stackTraceToString())
      return
    } catch (e: NullPointerException) {
      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG,
          "CloudBridge Settings API response is not a valid json: \n%s ",
          e.stackTraceToString())
      return
    }

    if (url == null || datasetID == null || accessKey == null) {
      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG,
          "CloudBridge Settings API response doesn't have valid data")

      return
    }

    try {
      AppEventsConversionsAPITransformerWebRequests.configure(datasetID, url, accessKey)

      savedCloudBridgeCredentials = config
    } catch (e: MalformedURLException) {
      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG,
          "CloudBridge Settings API response doesn't have valid url\n %s ",
          e.stackTraceToString())

      return
    }

    this.isEnabled =
        if (config[SettingsAPIFields.ENABLED.rawValue] != null)
            config[SettingsAPIFields.ENABLED.rawValue] as Boolean
        else false
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.cloudbridge

import com.facebook.LoggingBehavior
import com.facebook.appevents.AppEventsConstants
import com.facebook.internal.Logger
import com.facebook.internal.Utility.convertJSONArrayToList
import com.facebook.internal.Utility.convertJSONObjectToHashMap
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

// MARK: App parameters
// ======================================//======================================//======================================

enum class CustomEventField(val rawValue: String) {
  EVENT_TIME("_logTime"), // top level
  EVENT_NAME("_eventName"), // top level
  VALUE_TO_SUM("_valueToSum"),
  CONTENT_IDS("fb_content_id"),
  CONTENTS("fb_content"),
  CONTENT_TYPE("fb_content_type"),
  DESCRIPTION("fb_description"),
  LEVEL("fb_level"),
  MAX_RATING_VALUE("fb_max_rating_value"),
  NUM_ITEMS("fb_num_items"),
  PAYMENT_INFO_AVAILABLE("fb_payment_info_available"),
  REGISTRATION_METHOD("fb_registration_method"),
  SEARCH_STRING("fb_search_string"),
  SUCCESS("fb_success"),
  ORDER_ID("fb_order_id"),
  AD_TYPE("ad_type"),
  CURRENCY("fb_currency");

  internal companion object {
    operator fun invoke(rawValue: String): CustomEventField? =
        values().firstOrNull { it.rawValue == rawValue }
  }
}

enum class AppEventType {
  MOBILE_APP_INSTALL,
  CUSTOM,
  OTHER;

  internal companion object {
    operator fun invoke(rawValue: String): AppEventType = run {
      when (rawValue) {
        "MOBILE_APP_INSTALL" -> MOBILE_APP_INSTALL
        "CUSTOM_APP_EVENTS" -> CUSTOM
        else -> OTHER
      }
    }
  }
}

enum class AppEventUserAndAppDataField(val rawValue: String) {
  // user data fields
  ANON_ID("anon_id"),
  APP_USER_ID("app_user_id"),
  ADVERTISER_ID("advertiser_id"),
  PAGE_ID("page_id"),
  PAGE_SCOPED_USER_ID("page_scoped_user_id"),
  USER_DATA("ud"),

  // app data fields
  ADV_TE("advertiser_tracking_enabled"),
  APP_TE("application_tracking_enabled"),
  CONSIDER_VIEWS("consider_views"),
  DEVICE_TOKEN("device_token"),
  EXT_INFO("extInfo"),
  INCLUDE_DWELL_DATA("include_dwell_data"),
  INCLUDE_VIDEO_DATA("include_video_data"),
  INSTALL_REFERRER("install_referrer"),
  INSTALLER_PACKAGE("installer_package"),
  RECEIPT_DATA("receipt_data"),
  URL_SCHEMES("url_schemes");

  internal companion object {
    operator fun invoke(rawValue: String): AppEventUserAndAppDataField? =
        values().firstOrNull { it.rawValue == rawValue }
  }
}

// ======================================//======================================//======================================

// MARK: ConversionsAPI parameter JSON section
enum class ConversionsAPISection(val rawValue: String) {
  USER_DATA("user_data"),
  APP_DATA("app_data"),
  CUSTOM_DATA("custom_data"),
  CUSTOM_EVENTS("custom_events")
}

enum class ConversionsAPICustomEventField(val rawValue: String) {
  VALUE_TO_SUM("value"),
  EVENT_TIME("event_time"),
  EVENT_NAME("event_name"),
  CONTENT_IDS("content_ids"),
  CONTENTS("contents"),
  CONTENT_TYPE("content_type"),
  DESCRIPTION("description"),
  LEVEL("level"),
  MAX_RATING_VALUE("max_rating_value"),
  NUM_ITEMS("num_items"),
  PAYMENT_INFO_AVAILABLE("payment_info_available"),
  REGISTRATION_METHOD("registration_method"),
  SEARCH_STRING("search_string"),
  SUCCESS("success"),
  ORDER_ID("order_id"),
  AD_TYPE("ad_type"),
  CURRENCY("currency")
}

enum class ConversionsAPIUserAndAppDataField(val rawValue: String) {
  ANON_ID("anon_id"),
  FB_LOGIN_ID("fb_login_id"),
  MAD_ID("madid"),
  PAGE_ID("page_id"),
  PAGE_SCOPED_USER_ID("page_scoped_user_id"),
  USER_DATA("ud"),
  ADV_TE("advertiser_tracking_enabled"),
  APP_TE("application_tracking_enabled"),
  CONSIDER_VIEWS("consider_views"),
  DEVICE_TOKEN("device_token"),
  EXT_INFO("extInfo"),
  INCLUDE_DWELL_DATA("include_dwell_data"),
  INCLUDE_VIDEO_DATA("include_video_data"),
  INSTALL_REFERRER("install_referrer"),
  INSTALLER_PACKAGE("installer_package"),
  RECEIPT_DATA("receipt_data"),
  URL_SCHEMES("url_schemes")
}

enum class ConversionsAPIEventName(val rawValue: String) {
  UNLOCKED_ACHIEVEMENT("AchievementUnlocked"),
  ACTIVATED_APP("ActivateApp"),
  ADDED_PAYMENT_INFO("AddPaymentInfo"),
  ADDED_TO_CART("AddToCart"),
  ADDED_TO_WISHLIST("AddToWishlist"),
  COMPLETED_REGISTRATION("CompleteRegistration"),
  VIEWED_CONTENT("ViewContent"),
  INITIATED_CHECKOUT("InitiateCheckout"),
  ACHIEVED_LEVEL("LevelAchieved"),
  PURCHASED("Purchase"),
  RATED("Rate"),
  SEARCHED("Search"),
  SPENT_CREDITS("SpentCredits"),
  COMPLETED_TUTORIAL("TutorialCompletion"),
}

enum class OtherEventConstants(val rawValue: String) {
  EVENT("event"),
  ACTION_SOURCE("action_source"),
  APP("app"),
  MOBILE_APP_INSTALL("MobileAppInstall"),
  INSTALL_EVENT_TIME("install_timestamp")
}

// ======================================//======================================//======================================

// MARK: App Events to Conversions API Transformer dictionaries
object AppEventsConversionsAPITransformer {

  const val TAG = "AppEventsConversionsAPITransformer"

  data class SectionFieldMapping(
      var section: ConversionsAPISection,
      var field: ConversionsAPIUserAndAppDataField?
  )

  private val topLevelTransformations =
      mapOf(
          // user_data mapping
          AppEventUserAndAppDataField.ANON_ID to
              SectionFieldMapping(
                  ConversionsAPISection.USER_DATA, ConversionsAPIUserAndAppDataField.ANON_ID),
          AppEventUserAndAppDataField.APP_USER_ID to
              SectionFieldMapping(
                  ConversionsAPISection.USER_DATA, ConversionsAPIUserAndAppDataField.FB_LOGIN_ID),
          AppEventUserAndAppDataField.ADVERTISER_ID to
              SectionFieldMapping(
                  ConversionsAPISection.USER_DATA, ConversionsAPIUserAndAppDataField.MAD_ID),
          AppEventUserAndAppDataField.PAGE_ID to
              SectionFieldMapping(
                  ConversionsAPISection.USER_DATA, ConversionsAPIUserAndAppDataField.PAGE_ID),
          AppEventUserAndAppDataField.PAGE_SCOPED_USER_ID to
              SectionFieldMapping(
                  ConversionsAPISection.USER_DATA,
                  ConversionsAPIUserAndAppDataField.PAGE_SCOPED_USER_ID),

          // app_data mapping
          AppEventUserAndAppDataField.ADV_TE to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA, ConversionsAPIUserAndAppDataField.ADV_TE),
          AppEventUserAndAppDataField.APP_TE to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA, ConversionsAPIUserAndAppDataField.APP_TE),
          AppEventUserAndAppDataField.CONSIDER_VIEWS to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA, ConversionsAPIUserAndAppDataField.CONSIDER_VIEWS),
          AppEventUserAndAppDataField.DEVICE_TOKEN to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA, ConversionsAPIUserAndAppDataField.DEVICE_TOKEN),
          AppEventUserAndAppDataField.EXT_INFO to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA, ConversionsAPIUserAndAppDataField.EXT_INFO),
          AppEventUserAndAppDataField.INCLUDE_DWELL_DATA to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA,
                  ConversionsAPIUserAndAppDataField.INCLUDE_DWELL_DATA),
          AppEventUserAndAppDataField.INCLUDE_VIDEO_DATA to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA,
                  ConversionsAPIUserAndAppDataField.INCLUDE_VIDEO_DATA),
          AppEventUserAndAppDataField.INSTALL_REFERRER to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA,
                  ConversionsAPIUserAndAppDataField.INSTALL_REFERRER),
          AppEventUserAndAppDataField.INSTALLER_PACKAGE to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA,
                  ConversionsAPIUserAndAppDataField.INSTALLER_PACKAGE),
          AppEventUserAndAppDataField.RECEIPT_DATA to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA, ConversionsAPIUserAndAppDataField.RECEIPT_DATA),
          AppEventUserAndAppDataField.URL_SCHEMES to
              SectionFieldMapping(
                  ConversionsAPISection.APP_DATA, ConversionsAPIUserAndAppDataField.URL_SCHEMES),
          AppEventUserAndAppDataField.USER_DATA to
              SectionFieldMapping(ConversionsAPISection.USER_DATA, null),
      )

  data class SectionCustomEventFieldMapping(
      var section: ConversionsAPISection?,
      var field: ConversionsAPICustomEventField
  )

  @JvmField
  val customEventTransformations =
      mapOf(
          // custom_events mapping
          CustomEventField.EVENT_TIME to
              SectionCustomEventFieldMapping(null, ConversionsAPICustomEventField.EVENT_TIME),
          CustomEventField.EVENT_NAME to
              SectionCustomEventFieldMapping(null, ConversionsAPICustomEventField.EVENT_NAME),
          CustomEventField.VALUE_TO_SUM to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.VALUE_TO_SUM),
          CustomEventField.CONTENT_IDS to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA,
                  ConversionsAPICustomEventField
                      .CONTENT_IDS), // string to array conversion required
          CustomEventField.CONTENTS to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA,
                  ConversionsAPICustomEventField
                      .CONTENTS), // string to array conversion required, contents has an extra
          // field: price
          CustomEventField.CONTENT_TYPE to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.CONTENT_TYPE),
          CustomEventField.CURRENCY to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.CURRENCY),
          CustomEventField.DESCRIPTION to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.DESCRIPTION),
          CustomEventField.LEVEL to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.LEVEL),
          CustomEventField.MAX_RATING_VALUE to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA,
                  ConversionsAPICustomEventField.MAX_RATING_VALUE),
          CustomEventField.NUM_ITEMS to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.NUM_ITEMS),
          CustomEventField.PAYMENT_INFO_AVAILABLE to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA,
                  ConversionsAPICustomEventField.PAYMENT_INFO_AVAILABLE),
          CustomEventField.REGISTRATION_METHOD to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA,
                  ConversionsAPICustomEventField.REGISTRATION_METHOD),
          CustomEventField.SEARCH_STRING to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.SEARCH_STRING),
          CustomEventField.SUCCESS to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.SUCCESS),
          CustomEventField.ORDER_ID to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.ORDER_ID),
          CustomEventField.AD_TYPE to
              SectionCustomEventFieldMapping(
                  ConversionsAPISection.CUSTOM_DATA, ConversionsAPICustomEventField.AD_TYPE))

  @JvmField
  val standardEventTransformations =
      mapOf(
          AppEventsConstants.EVENT_NAME_UNLOCKED_ACHIEVEMENT to
              ConversionsAPIEventName.UNLOCKED_ACHIEVEMENT, // "achievementUnlocked",
          AppEventsConstants.EVENT_NAME_ACTIVATED_APP to ConversionsAPIEventName.ACTIVATED_APP,
          AppEventsConstants.EVENT_NAME_ADDED_PAYMENT_INFO to
              ConversionsAPIEventName.ADDED_PAYMENT_INFO,
          AppEventsConstants.EVENT_NAME_ADDED_TO_CART to ConversionsAPIEventName.ADDED_TO_CART,
          AppEventsConstants.EVENT_NAME_ADDED_TO_WISHLIST to
              ConversionsAPIEventName.ADDED_TO_WISHLIST,
          AppEventsConstants.EVENT_NAME_COMPLETED_REGISTRATION to
              ConversionsAPIEventName.COMPLETED_REGISTRATION,
          AppEventsConstants.EVENT_NAME_VIEWED_CONTENT to ConversionsAPIEventName.VIEWED_CONTENT,
          AppEventsConstants.EVENT_NAME_INITIATED_CHECKOUT to
              ConversionsAPIEventName.INITIATED_CHECKOUT,
          AppEventsConstants.EVENT_NAME_ACHIEVED_LEVEL to ConversionsAPIEventName.ACHIEVED_LEVEL,
          AppEventsConstants.EVENT_NAME_PURCHASED to
              ConversionsAPIEventName.PURCHASED, // fb_mobile_purchase
          AppEventsConstants.EVENT_NAME_RATED to ConversionsAPIEventName.RATED,
          AppEventsConstants.EVENT_NAME_SEARCHED to ConversionsAPIEventName.SEARCHED,
          AppEventsConstants.EVENT_NAME_SPENT_CREDITS to ConversionsAPIEventName.SPENT_CREDITS,
          AppEventsConstants.EVENT_NAME_COMPLETED_TUTORIAL to
              ConversionsAPIEventName.COMPLETED_TUTORIAL,
      )

  enum class DataProcessingParameterName(val rawValue: String) {
    OPTIONS("data_processing_options"),
    COUNTRY("data_processing_options_country"),
    STATE("data_processing_options_state");

    internal companion object {
      operator fun invoke(rawValue: String): DataProcessingParameterName? =
          values().firstOrNull { it.rawValue == rawValue }
    }
  }

  enum class ValueTransformationType {
    ARRAY,
    BOOL,
    INT;

    internal companion object {
      operator fun invoke(rawValue: String): ValueTransformationType? = run {
        when (rawValue) {
          AppEventUserAndAppDataField.EXT_INFO.rawValue -> ARRAY
          AppEventUserAndAppDataField.URL_SCHEMES.rawValue -> ARRAY
          CustomEventField.CONTENT_IDS.rawValue -> ARRAY
          CustomEventField.CONTENTS.rawValue -> ARRAY
          DataProcessingParameterName.OPTIONS.rawValue -> ARRAY
          AppEventUserAndAppDataField.ADV_TE.rawValue -> BOOL
          AppEventUserAndAppDataField.APP_TE.rawValue -> BOOL
          CustomEventField.EVENT_TIME.rawValue -> INT
          else -> null
        }
      }
    }
  }

  @JvmStatic
  internal fun transformValue(field: String, value: Any): Any? {
    val type = ValueTransformationType(field)
    val param = value as? String

    if (type == null || param == null) {
      return value
    }
    when (type) {
      ValueTransformationType.ARRAY -> {
        return try {
          val strList = convertJSONArrayToList(JSONArray(param))
          val arrayOfMaps = arrayListOf<Any?>()
          strList.forEach { str ->
            val final =
                try {
                  convertJSONObjectToHashMap(JSONObject(str))
                } catch (e: JSONException) {
                  // OK Ignore as we assume that it can be checked as another value type
                  try {
                    convertJSONArrayToList(JSONArray(str))
                  } catch (e: JSONException) {
                    // OK Ignore as we assume that it can be checked as another value type
                    str
                  }
                }
            arrayOfMaps.add(final)
          }
          return arrayOfMaps
        } catch (e: JSONException) {
          Logger.log(
              LoggingBehavior.APP_EVENTS,
              TAG,
              "\n transformEvents JSONException: \n%s\n%s",
              value,
              e)
        }
      }
      ValueTransformationType.BOOL -> {
        val coercedInteger = param.toString().toIntOrNull()
        return if (coercedInteger != null) {
          coercedInteger != 0
        } else {
          null
        }
      }
      ValueTransformationType.INT -> {
        return value.toString().toIntOrNull()
      }
    }
  }

  @JvmStatic
  internal fun transformEvents(appEvents: String): ArrayList<Map<String, Any>>? {

    // events is an array of json objects
    val events = arrayListOf<Map<String, Any>>()
    try {
      val jsonObjArr = convertJSONArrayToList(JSONArray(appEvents))

      jsonObjArr.forEach { jsonObjString ->
        val hashMap = convertJSONObjectToHashMap(JSONObject(jsonObjString))
        events.add(hashMap)
      }
    } catch (e: JSONException) {
      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG,
          "\n transformEvents JSONException: \n%s\n%s",
          appEvents,
          e)
      return null
    }

    if (events.isEmpty()) {
      return null
    }

    // return this
    val transformedEvents = ArrayList<Map<String, Any>>()

    events.forEach { eventMapObject ->
      val customData = mutableMapOf<String, Any>()
      val transformedEvent = mutableMapOf<String, Any>()

      // foe each keyVal pair in the event, convert it
      eventMapObject.keys.forEach inner@{ key ->
        val keyEnum = CustomEventField(key)
        val mapping = customEventTransformations[keyEnum]

        if (keyEnum == null || mapping == null) {
          return@inner // continue
        }

        val section = mapping.section
        if (section != null) {
          if (section == ConversionsAPISection.CUSTOM_DATA) {
            // TODO T101157887: Some of the parameters like fb_content_id, fb_content need to be
            // converted from string to array
            customData[mapping.field.rawValue] =
                transformValue(key, eventMapObject[key] as Any) as Any
          }
        } else { // Top level app event name to capi event name

          try {
            val capiFieldName = mapping.field.rawValue
            if (keyEnum == CustomEventField.EVENT_NAME && eventMapObject[key] as String? != null) {
              transformedEvent[capiFieldName] = transformEventName(eventMapObject[key] as String)
            } else
            // Top level app event name to capi event time
            if (keyEnum == CustomEventField.EVENT_TIME && eventMapObject[key] as Int? != null) {
              transformedEvent[capiFieldName] =
                  transformValue(key, eventMapObject[key] as Any) as Any
            }
          } catch (e: ClassCastException) {
            Logger.log(
                LoggingBehavior.APP_EVENTS,
                TAG,
                "\n transformEvents ClassCastException: \n %s ",
                e.stackTraceToString())
          }
        }
      }

      if (customData.isNotEmpty()) {
        transformedEvent[ConversionsAPISection.CUSTOM_DATA.rawValue] = customData
      }

      transformedEvents.add(transformedEvent as Map<String, Any>)
    }

    return transformedEvents
  }

  // MARK: user and app data transformations
  private fun transformAndUpdateAppData(
      appData: MutableMap<String, Any>,
      field: AppEventUserAndAppDataField,
      value: Any
  ) {
    val key = topLevelTransformations[field]?.field?.rawValue ?: return

    appData[key] = value
  }

  private fun transformAndUpdateUserData(
      userData: MutableMap<String, Any>,
      field: AppEventUserAndAppDataField,
      value: Any
  ) {

    if (field == AppEventUserAndAppDataField.USER_DATA) {
      try {
        val udParam = convertJSONObjectToHashMap(JSONObject(value as String))
        userData.putAll(udParam)
      } catch (e: JSONException) {

        Logger.log(
            LoggingBehavior.APP_EVENTS, TAG, "\n transformEvents JSONException: \n%s\n%s", value, e)
      }
    } else {
      val key = topLevelTransformations[field]?.field?.rawValue ?: return
      userData[key] = value
    }
  }

  internal fun transformAndUpdateAppAndUserData(
      userData: MutableMap<String, Any>,
      appData: MutableMap<String, Any>,
      field: AppEventUserAndAppDataField,
      value: Any
  ) {

    val section = topLevelTransformations[field]?.section ?: return
    when (section) {
      ConversionsAPISection.APP_DATA -> transformAndUpdateAppData(appData, field, value)
      ConversionsAPISection.USER_DATA -> transformAndUpdateUserData(userData, field, value)
      else -> return
    }
  }

  // MARK: events section transformations
  private fun transformEventName(input: String): String {

    return if (standardEventTransformations.containsKey(input)) {
      standardEventTransformations[input]?.rawValue ?: ""
    } else {
      input
    }
  }

  // MARK: combine transformed data
  internal fun combineCommonFields(
      userData: Map<String, Any>,
      appData: Map<String, Any>,
      restOfData: Map<String, Any>
  ): Map<String, Any> {

    val converted = mutableMapOf<String, Any>()
    converted[OtherEventConstants.ACTION_SOURCE.rawValue] = OtherEventConstants.APP.rawValue

    converted[ConversionsAPISection.USER_DATA.rawValue] = userData
    converted[ConversionsAPISection.APP_DATA.rawValue] = appData

    converted.putAll(restOfData)

    return converted
  }

  private fun combineAllTransformedDataForMobileAppInstall(
      commonFields: Map<String, Any>,
      eventTime: Any?
  ): List<Map<String, Any>>? {
    if (eventTime == null) {
      return null
    }
    val transformedEvent = mutableMapOf<String, Any>()

    // transformedEvent.merge(commonFields) { $1 }
    transformedEvent.putAll(commonFields)

    transformedEvent[ConversionsAPICustomEventField.EVENT_NAME.rawValue] =
        OtherEventConstants.MOBILE_APP_INSTALL.rawValue
    transformedEvent[ConversionsAPICustomEventField.EVENT_TIME.rawValue] = eventTime
    return listOf(transformedEvent)
  }

  private fun combineAllTransformedDataForCustom(
      commonFields: Map<String, Any>,
      customEvents: List<Map<String, Any>>
  ): List<Map<String, Any>>? {
    if (customEvents.isEmpty()) {
      return null
    }

    val transformedEvents = arrayListOf<Map<String, Any>>()

    customEvents.forEach { customEvent ->
      val customEventTransformed = mutableMapOf<String, Any>()

      // customEventTransformed.merge(commonFields) { $1 }
      customEventTransformed.putAll(commonFields)

      // customEventTransformed.merge(customEvent) { $1 }
      customEventTransformed.putAll(customEvent)

      // transformedEvents.append(customEventTransformed)
      transformedEvents.add(customEventTransformed)
    }

    return transformedEvents
  }

  internal fun combineAllTransformedData(
      eventType: AppEventType,
      userData: MutableMap<String, Any>,
      appData: MutableMap<String, Any>,
      restOfData: MutableMap<String, Any>,
      customEvents: List<Map<String, Any>>,
      eventTime: Any?
  ): List<Map<String, Any>>? {

    val commonFields = combineCommonFields(userData, appData, restOfData)

    return when (eventType) {
      AppEventType.MOBILE_APP_INSTALL ->
          combineAllTransformedDataForMobileAppInstall(commonFields, eventTime)
      AppEventType.CUSTOM -> combineAllTransformedDataForCustom(commonFields, customEvents)
      else -> null
    }
  }

  // customEvents is the equivalent of the request.tag, the actual "sub-events" under custom
  // event.
  // MARK: split app events parameters into user, app data and custom events
  private fun splitAppEventParameters(
      parameters: Map<String, Any>,
      userData: MutableMap<String, Any>,
      appData: MutableMap<String, Any>,
      customEvents: ArrayList<Map<String, Any>>,
      restOfData: MutableMap<String, Any>
  ): AppEventType {

    val eventTypeStr = parameters[OtherEventConstants.EVENT.rawValue]
    val eventType =
        AppEventType(
            eventTypeStr
                as String) // eventTypeStr?.let { AppEventType(it as String) } as AppEventType

    if (eventType == AppEventType.OTHER) {
      return eventType
    }

    parameters.forEach { (key, value) ->
      val field = AppEventUserAndAppDataField(key)

      if (field != null) { // either app data or user data
        transformAndUpdateAppAndUserData(userData, appData, field, value)
      } else {
        val isCustomEventsKey = (key == ConversionsAPISection.CUSTOM_EVENTS.rawValue)
        val isCustomEventsData = value is String

        if (eventType == AppEventType.CUSTOM && isCustomEventsKey && isCustomEventsData) {
          val events = transformEvents(value as String)
          if (events != null) {
            customEvents.addAll(events)
          }
        } else if (DataProcessingParameterName(key) != null) {
          restOfData[key] = value
        }
      }
    }
    return eventType
  }

  // MARK: main function
  internal fun conversionsAPICompatibleEvent(
      parameters: Map<String, Any>
  ): List<Map<String, Any>>? {
    val userData = mutableMapOf<String, Any>()
    val appData = mutableMapOf<String, Any>()
    val customEvents = arrayListOf<Map<String, Any>>()
    val restOfData = mutableMapOf<String, Any>()

    val eventType =
        splitAppEventParameters(
            parameters,
            userData,
            appData,
            customEvents,
            restOfData,
        )

    if (eventType == AppEventType.OTHER) {
      return null
    }

    return combineAllTransformedData(
        eventType,
        userData,
        appData,
        restOfData,
        customEvents,
        parameters[OtherEventConstants.INSTALL_EVENT_TIME.rawValue])
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.cloudbridge

import com.facebook.GraphRequest
import com.facebook.LoggingBehavior
import com.facebook.internal.Logger
import com.facebook.internal.Utility.convertJSONObjectToHashMap
import com.facebook.internal.Utility.runOnNonUiThread
import java.io.BufferedOutputStream
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import kotlin.math.max
import kotlin.math.min
import org.json.JSONArray
import org.json.JSONObject

object AppEventsConversionsAPITransformerWebRequests {

  data class CloudBridgeCredentials(
      val datasetID: String,
      val cloudBridgeURL: String,
      val accessKey: String
  )

  // Note we do not use canonical name due to Kotlin logger tag length limit.
  // Disabling limit has risks
  // https://stackoverflow.com/questions/28168622/the-logging-tag-can-be-at-most-23-characters
  private const val TAG = "CAPITransformerWebRequests"
  internal const val MAX_CACHED_TRANSFORMED_EVENTS = 1000
  private const val MAX_PROCESSED_TRANSFORMED_EVENTS = 10
  private const val TIMEOUT_INTERVAL = 60000
  private val ACCEPTABLE_HTTP_RESPONSE =
      hashSetOf(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED)
  private val RETRY_EVENTS_HTTP_RESPONSE =
      hashSetOf(HttpURLConnection.HTTP_UNAVAILABLE, HttpURLConnection.HTTP_GATEWAY_TIMEOUT, 429)
  // 429 too many requests

  internal lateinit var credentials: CloudBridgeCredentials
  internal lateinit var transformedEvents: MutableList<Map<String, Any>>

  internal const val MAX_RETRY_COUNT = 5
  internal var currentRetryCount: Int = 0

  @JvmStatic
  /**
   * Set the CAPI G Endpoint. Overrides any existing settings.
   * @property datasetID CAPI G datasetID
   * @property url CAPI G server url
   * @property accessKey CAPI G secret access key
   */
  fun configure(datasetID: String, url: String, accessKey: String) {
    Logger.log(
        LoggingBehavior.APP_EVENTS,
        TAG,
        " \n\nCloudbridge Configured: \n================\ndatasetID: %s\nurl: %s\naccessKey: %s\n\n",
        datasetID,
        url,
        accessKey)

    credentials = CloudBridgeCredentials(datasetID, url, accessKey)
    transformedEvents = mutableListOf()
  }

  @JvmStatic
  /**
   * Get the currently set credentials
   * @return credentials
   */
  fun getCredentials(): String? {

    return try {
      credentials?.toString()
    } catch (e: UninitializedPropertyAccessException) {
      null
    }
  }

  @JvmStatic
  /**
   * Take a graph request, extract the App Events within it and transform them. Then send it to the
   * cb endpoint.
   *
   * @property request GraphRequest to convert and send to CAPI G
   */
  fun transformGraphRequestAndSendToCAPIGEndPoint(request: GraphRequest) {

    runOnNonUiThread {
      val graphPathComponents = request.graphPath?.split("/")
      if (graphPathComponents == null || graphPathComponents.size != 2) {
        Logger.log(
            LoggingBehavior.DEVELOPER_ERRORS,
            TAG,
            "\n GraphPathComponents Error when logging: \n%s",
            request)
        return@runOnNonUiThread
      }
      val cbEndpoint: String
      try {

        val cloudBridgeURL = this.credentials.cloudBridgeURL
        val datasetID = this.credentials.datasetID

        cbEndpoint = "$cloudBridgeURL/capi/$datasetID/events"
      } catch (e: UninitializedPropertyAccessException) {
        Logger.log(
            LoggingBehavior.DEVELOPER_ERRORS,
            TAG,
            "\n Credentials not initialized Error when logging: \n%s",
            e)

        return@runOnNonUiThread
      }
      val transformed = transformAppEventRequestForCAPIG(request) ?: return@runOnNonUiThread
      appendEvents(transformed)
      val eventToProcessCount = min(transformedEvents.count(), MAX_PROCESSED_TRANSFORMED_EVENTS)

      // create copy of the events we are going to send off
      val processedEvents = transformedEvents.slice(IntRange(0, eventToProcessCount - 1))
      transformedEvents.subList(0, eventToProcessCount).clear()

      val jsonArr = JSONArray(processedEvents)
      val jsonMap = mutableMapOf<String, Any?>()
      jsonMap["data"] = jsonArr
      jsonMap["accessKey"] = this.credentials.accessKey
      val jsonBodyStr = JSONObject(jsonMap)

      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG,
          "\nTransformed_CAPI_JSON:\n" +
              "URL: %s\n" +
              "FROM=========\n" +
              "%s\n" +
              ">>>>>>TO>>>>>>\n" +
              "%s\n" +
              "=============\n",
          cbEndpoint,
          request,
          jsonBodyStr.toString(2))

      makeHttpRequest(
          cbEndpoint,
          "POST",
          jsonBodyStr.toString(),
          mapOf("Content-Type" to "application/json"),
          TIMEOUT_INTERVAL,
      ) { _: String?, responseCode: Int? ->
        runOnNonUiThread {
          if (!ACCEPTABLE_HTTP_RESPONSE.contains(responseCode)) {
            handleError(responseCode, processedEvents, MAX_RETRY_COUNT)
          }
        }
      }
    }
  }

  private fun transformAppEventRequestForCAPIG(request: GraphRequest): List<Map<String, Any>>? {
    // tag is custom data
    // graphRequest is the common data
    val graphRequestObjJSON = request.graphObject

    if (graphRequestObjJSON != null) {
      val requestData = convertJSONObjectToHashMap(graphRequestObjJSON).toMutableMap()

      // add the custom event data in
      requestData["custom_events"] = request.tag as Any

      val sb = StringBuilder()
      for (key in requestData.keys) {
        sb.append(key)
        sb.append(" : ")
        sb.append(requestData[key])
        sb.append(System.getProperty("line.separator"))
      }
      Logger.log(LoggingBehavior.APP_EVENTS, TAG, "\nGraph Request data: \n\n%s \n\n", sb)

      return AppEventsConversionsAPITransformer.conversionsAPICompatibleEvent(requestData)
    }

    return null
  }

  internal fun handleError(
      responseCode: Int?,
      processedEvents: List<Map<String, Any>>,
      maxRetryCount: Int = 5
  ) {

    // We received an error, but it's not server error.
    // we'll re-append the events we processed to the event queue
    if (RETRY_EVENTS_HTTP_RESPONSE.contains(responseCode)) {

      if (this.currentRetryCount >= maxRetryCount) {
        transformedEvents.clear()
        this.currentRetryCount = 0
      } else {
        transformedEvents.addAll(0, processedEvents)
        this.currentRetryCount++
      }
    }
  }

  internal fun appendEvents(events: List<Map<String, Any>>?) {
    if (events != null) {
      transformedEvents.addAll(events)
    }
    val overCacheLimitCount = max(0, transformedEvents.count() - MAX_CACHED_TRANSFORMED_EVENTS)
    if (overCacheLimitCount > 0) {
      transformedEvents =
          transformedEvents.drop(overCacheLimitCount) as MutableList<Map<String, Any>>
    }
  }

  internal fun makeHttpRequest(
      urlStr: String,
      requestMethod: String,
      jsonBodyStr: String?,
      requestProperties: Map<String, String>?,
      timeOutInterval: Int = 60000,
      requestCallback: ((requestResult: String?, responseCode: Int?) -> Unit)?
  ) {
    try {

      val url = URL(urlStr)
      val httpURLConnection = url.openConnection() as HttpURLConnection
      httpURLConnection.requestMethod = requestMethod
      requestProperties?.keys?.forEach { key ->
        httpURLConnection.setRequestProperty(key, requestProperties[key])
      }

      httpURLConnection.doOutput =
          httpURLConnection.requestMethod.equals("POST") ||
              httpURLConnection.requestMethod.equals("PUT")
      httpURLConnection.connectTimeout = timeOutInterval

      val output: OutputStream = BufferedOutputStream(httpURLConnection.outputStream)
      val writer = BufferedWriter(OutputStreamWriter(output, "UTF-8"))
      writer.write(jsonBodyStr)
      writer.flush()
      writer.close()
      output.close()

      val connResponseSB = StringBuilder()
      if (ACCEPTABLE_HTTP_RESPONSE.contains(httpURLConnection.responseCode)) {
        BufferedReader(InputStreamReader(httpURLConnection.inputStream, "UTF-8")).use {
            bufferedReader ->
          var line: String?
          while (bufferedReader.readLine().also { line = it } != null) {
            connResponseSB.append(line)
          }
        }
      }

      val connResponse = connResponseSB.toString()
      Logger.log(
          LoggingBehavior.APP_EVENTS,
          TAG,
          "\nResponse Received: \n%s\n%s",
          connResponse,
          httpURLConnection.responseCode)

      if (requestCallback !== null) {
        requestCallback(connResponse, httpURLConnection.responseCode)
      }
    } catch (e: java.net.UnknownHostException) {
      // referencing HTTPURLConnection and having no internet connection
      // https://stackoverflow.com/questions/20361567/httpurlconnection-and-no-internet

      Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Connection failed, retrying: \n%s", e.toString())

      if (requestCallback !== null) {
        requestCallback(null, HttpURLConnection.HTTP_UNAVAILABLE)
      }
    } catch (e: java.io.IOException) {
      Logger.log(LoggingBehavior.DEVELOPER_ERRORS, TAG, "Send to server failed: \n%s", e.toString())
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless

import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import androidx.annotation.RestrictTo
import com.facebook.FacebookSdk.getApplicationContext
import com.facebook.FacebookSdk.getExecutor
import com.facebook.appevents.AppEventsConstants
import com.facebook.appevents.AppEventsLogger.Companion.newLogger
import com.facebook.appevents.codeless.CodelessMatcher.Companion.getParameters
import com.facebook.appevents.codeless.internal.Constants
import com.facebook.appevents.codeless.internal.EventBinding
import com.facebook.appevents.codeless.internal.ViewHierarchy
import com.facebook.appevents.internal.AppEventUtility.normalizePrice
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.lang.ref.WeakReference

@AutoHandleExceptions
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object CodelessLoggingEventListener {
  @JvmStatic
  fun getOnClickListener(
      mapping: EventBinding,
      rootView: View,
      hostView: View
  ): AutoLoggingOnClickListener {
    return AutoLoggingOnClickListener(mapping, rootView, hostView)
  }

  @JvmStatic
  fun getOnItemClickListener(
      mapping: EventBinding,
      rootView: View,
      hostView: AdapterView<*>
  ): AutoLoggingOnItemClickListener {
    return AutoLoggingOnItemClickListener(mapping, rootView, hostView)
  }

  @JvmStatic
  internal fun logEvent(mapping: EventBinding, rootView: View, hostView: View) {
    val eventName = mapping.eventName
    val parameters = getParameters(mapping, rootView, hostView)
    updateParameters(parameters)
    getExecutor().execute {
      val context = getApplicationContext()
      val appEventsLogger = newLogger(context)
      appEventsLogger.logEvent(eventName, parameters)
    }
  }

  internal fun updateParameters(parameters: Bundle) {
    val value = parameters.getString(AppEventsConstants.EVENT_PARAM_VALUE_TO_SUM)
    if (value != null) {
      parameters.putDouble(AppEventsConstants.EVENT_PARAM_VALUE_TO_SUM, normalizePrice(value))
    }
    parameters.putString(Constants.IS_CODELESS_EVENT_KEY, "1")
  }

  class AutoLoggingOnClickListener(mapping: EventBinding, rootView: View, hostView: View) :
      View.OnClickListener {
    override fun onClick(view: View) {
      // If there is an existing listener and its not the one of AutoLoggingOnClickListener
      // then call its onClick function
      existingOnClickListener?.onClick(view)
      val rootViewValue = rootView.get()
      val hostViewValue = hostView.get()
      if (rootViewValue != null && hostViewValue != null) {
        logEvent(mapping as EventBinding, rootViewValue, hostViewValue)
      }
    }

    private var mapping: EventBinding = mapping
    private var hostView: WeakReference<View> = WeakReference(hostView)
    private var rootView: WeakReference<View> = WeakReference(rootView)
    private var existingOnClickListener: View.OnClickListener? =
        ViewHierarchy.getExistingOnClickListener(hostView)
    var supportCodelessLogging = true
  }

  class AutoLoggingOnItemClickListener(
      mapping: EventBinding,
      rootView: View,
      hostView: AdapterView<*>
  ) : AdapterView.OnItemClickListener {
    override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
      existingOnItemClickListener?.onItemClick(parent, view, position, id)
      val rootViewValue = rootView.get()
      val hostViewValue = hostView.get()
      if (rootViewValue != null && hostViewValue != null) {
        logEvent(mapping, rootViewValue, hostViewValue)
      }
    }

    private var mapping: EventBinding = mapping
    private var hostView: WeakReference<AdapterView<*>> = WeakReference(hostView)
    private var rootView: WeakReference<View> = WeakReference(rootView)
    private var existingOnItemClickListener: AdapterView.OnItemClickListener? =
        hostView.onItemClickListener
    var supportCodelessLogging = true
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless

import android.app.Activity
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import androidx.annotation.RestrictTo
import com.facebook.FacebookSdk.getApplicationContext
import com.facebook.FacebookSdk.getApplicationId
import com.facebook.FacebookSdk.getCodelessSetupEnabled
import com.facebook.FacebookSdk.getExecutor
import com.facebook.GraphRequest
import com.facebook.appevents.codeless.internal.Constants
import com.facebook.appevents.internal.AppEventUtility.isEmulator
import com.facebook.core.BuildConfig
import com.facebook.internal.AttributionIdentifiers.Companion.getAttributionIdentifiers
import com.facebook.internal.FetchedAppSettingsManager.getAppSettingsWithoutQuery
import com.facebook.internal.Utility.currentLocale
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.util.Locale
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.jvm.Volatile
import org.json.JSONArray

@AutoHandleExceptions
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object CodelessManager {
  private val viewIndexingTrigger = ViewIndexingTrigger()
  private var sensorManager: SensorManager? = null
  private var viewIndexer: ViewIndexer? = null
  private var deviceSessionID: String? = null
  private val isCodelessEnabled = AtomicBoolean(true)
  private val isAppIndexingEnabled = AtomicBoolean(false)

  @Volatile private var isCheckingSession = false
  @JvmStatic
  fun onActivityResumed(activity: Activity) {
    if (!isCodelessEnabled.get()) {
      return
    }
    CodelessMatcher.getInstance().add(activity)
    val applicationContext = activity.applicationContext
    val appId = getApplicationId()
    val appSettings = getAppSettingsWithoutQuery(appId)
    if (appSettings?.codelessEventsEnabled == true || isDebugOnEmulator()) {
      val sensorManager =
          applicationContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager? ?: return
      this.sensorManager = sensorManager
      val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
      val viewIndexer = ViewIndexer(activity)
      this.viewIndexer = viewIndexer
      viewIndexingTrigger.setOnShakeListener {
        val codelessEventsEnabled = appSettings != null && appSettings.codelessEventsEnabled
        val codelessSetupEnabled = (getCodelessSetupEnabled() || BuildConfig.DEBUG && isEmulator())
        if (codelessEventsEnabled && codelessSetupEnabled) {
          checkCodelessSession(appId)
        }
      }
      sensorManager.registerListener(
          viewIndexingTrigger, accelerometer, SensorManager.SENSOR_DELAY_UI)
      if (appSettings != null && appSettings.codelessEventsEnabled) {
        viewIndexer.schedule()
      }
    }
    if (isDebugOnEmulator() && !isAppIndexingEnabled.get()) {
      // Check session on start when app launched
      // on emulator and built in DEBUG mode
      checkCodelessSession(appId)
    }
  }

  @JvmStatic
  fun onActivityPaused(activity: Activity) {
    if (!isCodelessEnabled.get()) {
      return
    }
    CodelessMatcher.getInstance().remove(activity)
    viewIndexer?.unschedule()
    sensorManager?.unregisterListener(viewIndexingTrigger)
  }

  @JvmStatic
  fun onActivityDestroyed(activity: Activity) {
    CodelessMatcher.getInstance().destroy(activity)
  }

  @JvmStatic
  fun enable() {
    isCodelessEnabled.set(true)
  }

  @JvmStatic
  fun disable() {
    isCodelessEnabled.set(false)
  }

  private fun checkCodelessSession(applicationId: String?) {
    if (isCheckingSession) {
      return
    }
    isCheckingSession = true
    getExecutor().execute {
      val requestParameters = Bundle()
      val context = getApplicationContext()
      val identifiers = getAttributionIdentifiers(context)
      val extInfoArray = JSONArray()
      extInfoArray.put(if (Build.MODEL != null) Build.MODEL else "")
      if (identifiers?.androidAdvertiserId != null) {
        extInfoArray.put(identifiers.androidAdvertiserId)
      } else {
        extInfoArray.put("")
      }
      extInfoArray.put(if (BuildConfig.DEBUG) "1" else "0")
      extInfoArray.put(if (isEmulator()) "1" else "0")
      // Locale
      val locale = currentLocale
      extInfoArray.put(locale.language + "_" + locale.country)
      val extInfo = extInfoArray.toString()
      requestParameters.putString(Constants.DEVICE_SESSION_ID, getCurrentDeviceSessionID())
      requestParameters.putString(Constants.EXTINFO, extInfo)
      val request =
          GraphRequest.newPostRequestWithBundle(
              null,
              String.format(Locale.US, "%s/app_indexing_session", applicationId),
              requestParameters,
              null)
      val res = request.executeAndWait()
      val jsonRes = res.getJSONObject()
      isAppIndexingEnabled.set(
          jsonRes != null && jsonRes.optBoolean(Constants.APP_INDEXING_ENABLED, false))
      if (!isAppIndexingEnabled.get()) {
        deviceSessionID = null
      } else {
        viewIndexer?.schedule()
      }
      isCheckingSession = false
    }
  }

  private fun isDebugOnEmulator(): Boolean {
    return BuildConfig.DEBUG && isEmulator()
  }

  @JvmStatic
  internal fun getCurrentDeviceSessionID(): String {
    if (null == deviceSessionID) {
      deviceSessionID = UUID.randomUUID().toString()
    }
    return deviceSessionID as String
  }

  @JvmStatic
  internal fun getIsAppIndexingEnabled(): Boolean {
    return isAppIndexingEnabled.get()
  }

  @JvmStatic
  internal fun updateAppIndexing(appIndexingEnabled: Boolean) {
    isAppIndexingEnabled.set(appIndexingEnabled)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless

import android.app.Activity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.AdapterView
import android.widget.ListView
import androidx.annotation.UiThread
import com.facebook.FacebookException
import com.facebook.FacebookSdk.getApplicationId
import com.facebook.appevents.codeless.CodelessLoggingEventListener.AutoLoggingOnClickListener
import com.facebook.appevents.codeless.CodelessLoggingEventListener.AutoLoggingOnItemClickListener
import com.facebook.appevents.codeless.internal.Constants
import com.facebook.appevents.codeless.internal.EventBinding
import com.facebook.appevents.codeless.internal.PathComponent
import com.facebook.appevents.codeless.internal.ViewHierarchy
import com.facebook.appevents.internal.AppEventUtility.getRootView
import com.facebook.internal.FetchedAppSettingsManager.getAppSettingsWithoutQuery
import com.facebook.internal.InternalSettings.isUnityApp
import com.facebook.internal.Utility.coerceValueIfNullOrEmpty
import com.facebook.internal.Utility.logd
import com.facebook.internal.Utility.sha256hash
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.lang.ref.WeakReference
import java.util.Collections
import java.util.WeakHashMap
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet

@AutoHandleExceptions
internal class CodelessMatcher private constructor() {
  private val uiThreadHandler: Handler = Handler(Looper.getMainLooper())
  private val activitiesSet: MutableSet<Activity> = Collections.newSetFromMap(WeakHashMap())
  private val viewMatchers: MutableSet<ViewMatcher> = mutableSetOf()
  private var listenerSet: HashSet<String> = hashSetOf()
  private val activityToListenerMap: HashMap<Int, HashSet<String>> = hashMapOf()
  @UiThread
  fun add(activity: Activity) {
    if (isUnityApp) {
      return
    }
    if (Thread.currentThread() !== Looper.getMainLooper().thread) {
      throw FacebookException("Can't add activity to CodelessMatcher on non-UI thread")
    }
    activitiesSet.add(activity)
    listenerSet.clear()
    activityToListenerMap[activity.hashCode()]?.let { listenerSet = it }
    startTracking()
  }

  @UiThread
  fun remove(activity: Activity) {
    if (isUnityApp) {
      return
    }
    if (Thread.currentThread() !== Looper.getMainLooper().thread) {
      throw FacebookException("Can't remove activity from CodelessMatcher on non-UI thread")
    }
    activitiesSet.remove(activity)
    viewMatchers.clear()
    activityToListenerMap[activity.hashCode()] = listenerSet.clone() as HashSet<String>
    listenerSet.clear()
  }

  @UiThread
  fun destroy(activity: Activity) {
    activityToListenerMap.remove(activity.hashCode())
  }

  private fun startTracking() {
    if (Thread.currentThread() === Looper.getMainLooper().thread) {
      matchViews()
    } else {
      uiThreadHandler.post { matchViews() }
    }
  }

  private fun matchViews() {
    for (activity in activitiesSet) {
      if (null != activity) {
        val rootView = getRootView(activity)
        val activityName = activity.javaClass.simpleName
        val matcher = ViewMatcher(rootView, uiThreadHandler, listenerSet, activityName)
        viewMatchers.add(matcher)
      }
    }
  }

  class MatchedView(view: View, viewMapKey: String) {
    private val view: WeakReference<View>?
    val viewMapKey: String
    fun getView(): View? {
      return view?.get()
    }

    init {
      this.view = WeakReference(view)
      this.viewMapKey = viewMapKey
    }
  }

  @UiThread
  class ViewMatcher(
      rootView: View?,
      handler: Handler,
      listenerSet: HashSet<String>,
      activityName: String
  ) : ViewTreeObserver.OnGlobalLayoutListener, ViewTreeObserver.OnScrollChangedListener, Runnable {
    private val rootView: WeakReference<View?> = WeakReference(rootView)
    private var eventBindings: List<EventBinding>? = null
    private val handler: Handler = handler
    private val listenerSet: HashSet<String> = listenerSet
    private val activityName: String = activityName
    override fun run() {
      val appId = getApplicationId()
      val appSettings = getAppSettingsWithoutQuery(appId)
      if (appSettings == null || !appSettings.codelessEventsEnabled) {
        return
      }
      eventBindings = EventBinding.parseArray(appSettings.eventBindings)
      if (eventBindings != null) {
        val rootView = rootView.get() ?: return
        val observer = rootView.viewTreeObserver
        if (observer.isAlive) {
          observer.addOnGlobalLayoutListener(this)
          observer.addOnScrollChangedListener(this)
        }
        startMatch()
      }
    }

    override fun onGlobalLayout() {
      startMatch()
    }

    override fun onScrollChanged() {
      startMatch()
    }

    private fun startMatch() {
      eventBindings?.let {
        if (rootView.get() != null) {
          for (i in it.indices) {
            val binding = it[i]
            findView(binding, rootView.get())
          }
        }
      }
    }

    private fun findView(mapping: EventBinding?, rootView: View?) {
      if (mapping == null || rootView == null) {
        return
      }
      if (!mapping.activityName.isNullOrEmpty() && mapping.activityName != activityName) {
        return
      }
      val path = mapping.viewPath
      if (path.size > Constants.MAX_TREE_DEPTH) {
        return
      }
      val matchedViews = findViewByPath(mapping, rootView, path, 0, -1, activityName)
      for (view in matchedViews) {
        attachListener(view, rootView, mapping)
      }
    }

    private fun attachListener(matchedView: MatchedView, rootView: View, mapping: EventBinding?) {
      if (mapping == null) {
        return
      }
      try {
        val view = matchedView.getView() ?: return
        // If it's React Native Button, then attach React Native OnTouchListener
        val rctRootView = ViewHierarchy.findRCTRootView(view)
        if (null != rctRootView && ViewHierarchy.isRCTButton(view, rctRootView)) {
          attachRCTListener(matchedView, rootView, mapping)
          return
        }
        // Skip if the view comes from React Native
        if (view.javaClass.name.startsWith("com.facebook.react")) {
          return
        }
        if (view !is AdapterView<*>) {
          // attach onClickListener
          attachOnClickListener(matchedView, rootView, mapping)
        } else if (view is ListView) {
          // attach AdapterView onItemClickListener
          attachOnItemClickListener(matchedView, rootView, mapping)
        }
      } catch (e: Exception) {
        logd(TAG, e)
      }
    }

    private fun attachOnClickListener(
        matchedView: MatchedView,
        rootView: View,
        mapping: EventBinding
    ) {
      val view = matchedView.getView() ?: return
      val mapKey = matchedView.viewMapKey
      val existingListener = ViewHierarchy.getExistingOnClickListener(view)
      val isCodelessListener = existingListener is AutoLoggingOnClickListener
      val listenerSupportCodelessLogging =
          isCodelessListener &&
              (existingListener as AutoLoggingOnClickListener).supportCodelessLogging
      if (!listenerSet.contains(mapKey) && !listenerSupportCodelessLogging) {
        val listener: View.OnClickListener =
            CodelessLoggingEventListener.getOnClickListener(mapping, rootView, view)
        view.setOnClickListener(listener)
        listenerSet.add(mapKey)
      }
    }

    private fun attachOnItemClickListener(
        matchedView: MatchedView,
        rootView: View,
        mapping: EventBinding
    ) {
      val view = matchedView.getView() as AdapterView<*>? ?: return
      val mapKey = matchedView.viewMapKey
      val existingListener = view.onItemClickListener
      val isCodelessListener = existingListener is AutoLoggingOnItemClickListener
      val listenerSupportCodelessLogging =
          isCodelessListener &&
              (existingListener as AutoLoggingOnItemClickListener).supportCodelessLogging
      if (!listenerSet.contains(mapKey) && !listenerSupportCodelessLogging) {
        val listener: AdapterView.OnItemClickListener =
            CodelessLoggingEventListener.getOnItemClickListener(mapping, rootView, view)
        view.onItemClickListener = listener
        listenerSet.add(mapKey)
      }
    }

    private fun attachRCTListener(matchedView: MatchedView, rootView: View, mapping: EventBinding) {
      val view = matchedView.getView() ?: return
      val mapKey = matchedView.viewMapKey
      val existingListener = ViewHierarchy.getExistingOnTouchListener(view)
      val isRCTCodelessListener =
          existingListener is RCTCodelessLoggingEventListener.AutoLoggingOnTouchListener
      val listenerSupportCodelessLogging =
          isRCTCodelessListener &&
              (existingListener as RCTCodelessLoggingEventListener.AutoLoggingOnTouchListener)
                  .supportCodelessLogging
      if (!listenerSet.contains(mapKey) && !listenerSupportCodelessLogging) {
        val listener: View.OnTouchListener =
            RCTCodelessLoggingEventListener.getOnTouchListener(mapping, rootView, view)
        view.setOnTouchListener(listener)
        listenerSet.add(mapKey)
      }
    }

    companion object {
      @JvmStatic
      fun findViewByPath(
          mapping: EventBinding?,
          view: View?,
          path: List<PathComponent>,
          level: Int,
          index: Int,
          mapKey: String
      ): List<MatchedView> {
        val mapKey = "$mapKey.$index"
        val result: MutableList<MatchedView> = ArrayList()
        if (null == view) {
          return result
        }
        if (level >= path.size) {
          // Match all children views if their parent view is matched
          result.add(MatchedView(view, mapKey))
        } else {
          val pathElement = path[level]
          if (pathElement.className == PARENT_CLASS_NAME) {
            val parent = view.parent
            if (parent is ViewGroup) {
              val visibleViews = findVisibleChildren(parent)
              val childCount = visibleViews.size
              for (i in 0 until childCount) {
                val child = visibleViews[i]
                val matchedViews = findViewByPath(mapping, child, path, level + 1, i, mapKey)
                result.addAll(matchedViews)
              }
            }
            return result
          } else if (pathElement.className == CURRENT_CLASS_NAME) {
            // Set self as selected element
            result.add(MatchedView(view, mapKey))
            return result
          }
          if (!isTheSameView(view, pathElement, index)) {
            return result
          }

          // Found it!
          if (level == path.size - 1) {
            result.add(MatchedView(view, mapKey))
          }
        }
        if (view is ViewGroup) {
          val visibleViews = findVisibleChildren(view)
          val childCount = visibleViews.size
          for (i in 0 until childCount) {
            val child = visibleViews[i]
            val matchedViews = findViewByPath(mapping, child, path, level + 1, i, mapKey)
            result.addAll(matchedViews)
          }
        }
        return result
      }

      private fun isTheSameView(targetView: View, pathElement: PathComponent, index: Int): Boolean {
        if (pathElement.index != -1 && index != pathElement.index) {
          return false
        }
        if (targetView.javaClass.canonicalName != pathElement.className) {
          if (pathElement.className.matches(".*android\\..*".toRegex())) {
            val names = pathElement.className.split(".")
            if (names.isNotEmpty()) {
              val simpleName = names[names.size - 1]
              if (targetView.javaClass.simpleName != simpleName) {
                return false
              }
            } else {
              return false
            }
          } else {
            return false
          }
        }
        if (pathElement.matchBitmask and PathComponent.MatchBitmaskType.ID.value > 0) {
          if (pathElement.id != targetView.id) {
            return false
          }
        }
        if (pathElement.matchBitmask and PathComponent.MatchBitmaskType.TEXT.value > 0) {
          val pathText = pathElement.text
          val text = ViewHierarchy.getTextOfView(targetView)
          val hashedText = coerceValueIfNullOrEmpty(sha256hash(text), "")
          if (pathText != text && pathText != hashedText) {
            return false
          }
        }
        if (pathElement.matchBitmask and PathComponent.MatchBitmaskType.DESCRIPTION.value > 0) {
          val pathDesc = pathElement.description
          val targetDesc =
              if (targetView.contentDescription == null) ""
              else targetView.contentDescription.toString()
          val hashedDesc = coerceValueIfNullOrEmpty(sha256hash(targetDesc), "")
          if (pathDesc != targetDesc && pathDesc != hashedDesc) {
            return false
          }
        }
        if (pathElement.matchBitmask and PathComponent.MatchBitmaskType.HINT.value > 0) {
          val pathHint = pathElement.hint
          val targetHint = ViewHierarchy.getHintOfView(targetView)
          val hashedHint = coerceValueIfNullOrEmpty(sha256hash(targetHint), "")
          if (pathHint != targetHint && pathHint != hashedHint) {
            return false
          }
        }
        if (pathElement.matchBitmask and PathComponent.MatchBitmaskType.TAG.value > 0) {
          val tag = pathElement.tag
          val targetTag = if (targetView.tag == null) "" else targetView.tag.toString()
          val hashedTag = coerceValueIfNullOrEmpty(sha256hash(targetTag), "")
          if (tag != targetTag && tag != hashedTag) {
            return false
          }
        }
        return true
      }

      private fun findVisibleChildren(viewGroup: ViewGroup): List<View> {
        val visibleViews: MutableList<View> = ArrayList()
        val childCount = viewGroup.childCount
        for (i in 0 until childCount) {
          val child = viewGroup.getChildAt(i)
          if (child.visibility == View.VISIBLE) {
            visibleViews.add(child)
          }
        }
        return visibleViews
      }
    }

    init {
      this.handler.postDelayed(this, 200)
    }
  }

  companion object {
    private const val PARENT_CLASS_NAME = ".."
    private const val CURRENT_CLASS_NAME = "."
    private val TAG = CodelessMatcher::class.java.canonicalName
    private var codelessMatcher: CodelessMatcher? = null

    @Synchronized
    @JvmStatic
    fun getInstance(): CodelessMatcher {
      if (codelessMatcher == null) {
        codelessMatcher = CodelessMatcher()
      }
      return codelessMatcher as CodelessMatcher
    }

    @UiThread
    @JvmStatic
    fun getParameters(mapping: EventBinding?, rootView: View, hostView: View): Bundle {
      val params = Bundle()
      if (null == mapping) {
        return params
      }
      val parameters = mapping.viewParameters
      if (null != parameters) {
        for (component in parameters) {
          if (component.value != null && component.value.isNotEmpty()) {
            params.putString(component.name, component.value)
          } else if (component.path.size > 0) {
            var matchedViews: List<MatchedView>
            val pathType = component.pathType
            matchedViews =
                if (pathType == Constants.PATH_TYPE_RELATIVE) {
                  ViewMatcher.findViewByPath(
                      mapping, hostView, component.path, 0, -1, hostView.javaClass.simpleName)
                } else {
                  ViewMatcher.findViewByPath(
                      mapping, rootView, component.path, 0, -1, rootView.javaClass.simpleName)
                }
            for (view in matchedViews) {
              if (view.getView() == null) {
                continue
              }
              val text = ViewHierarchy.getTextOfView(view.getView())
              if (text.isNotEmpty()) {
                params.putString(component.name, text)
                break
              }
            }
          }
        }
      }
      return params
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless

import android.view.MotionEvent
import android.view.View
import com.facebook.appevents.codeless.CodelessLoggingEventListener.logEvent
import com.facebook.appevents.codeless.internal.EventBinding
import com.facebook.appevents.codeless.internal.ViewHierarchy.getExistingOnTouchListener
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.lang.ref.WeakReference

@AutoHandleExceptions
object RCTCodelessLoggingEventListener {
  @JvmStatic
  fun getOnTouchListener(
      mapping: EventBinding,
      rootView: View,
      hostView: View
  ): AutoLoggingOnTouchListener {
    return AutoLoggingOnTouchListener(mapping, rootView, hostView)
  }

  class AutoLoggingOnTouchListener(mapping: EventBinding, rootView: View, hostView: View) :
      View.OnTouchListener {
    override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
      val rootViewValue = rootView.get()
      val hostViewValue = hostView.get()
      if (rootViewValue != null &&
          hostViewValue != null &&
          motionEvent.action == MotionEvent.ACTION_UP) {
        logEvent(mapping, rootViewValue, hostViewValue)
      }

      // If there is an existing listener then call its onTouch function else return false
      return (existingOnTouchListener != null && existingOnTouchListener.onTouch(view, motionEvent))
    }

    private val mapping: EventBinding = mapping
    private val hostView: WeakReference<View> = WeakReference(hostView)
    private val rootView: WeakReference<View> = WeakReference(rootView)
    private val existingOnTouchListener: View.OnTouchListener? =
        getExistingOnTouchListener(hostView)
    var supportCodelessLogging = true
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless

import android.app.Activity
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import android.view.View
import androidx.annotation.RestrictTo
import com.facebook.AccessToken
import com.facebook.AccessToken.Companion.getCurrentAccessToken
import com.facebook.FacebookSdk.getApplicationId
import com.facebook.FacebookSdk.getExecutor
import com.facebook.GraphRequest
import com.facebook.GraphRequest.Companion.newPostRequest
import com.facebook.LoggingBehavior
import com.facebook.appevents.codeless.CodelessManager.getCurrentDeviceSessionID
import com.facebook.appevents.codeless.CodelessManager.getIsAppIndexingEnabled
import com.facebook.appevents.codeless.CodelessManager.updateAppIndexing
import com.facebook.appevents.codeless.internal.Constants
import com.facebook.appevents.codeless.internal.UnityReflection
import com.facebook.appevents.codeless.internal.ViewHierarchy
import com.facebook.appevents.internal.AppEventUtility.getAppVersion
import com.facebook.appevents.internal.AppEventUtility.getRootView
import com.facebook.internal.InternalSettings.isUnityApp
import com.facebook.internal.Logger.Companion.log
import com.facebook.internal.Utility.md5hash
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.io.ByteArrayOutputStream
import java.lang.ref.WeakReference
import java.util.Locale
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.Callable
import java.util.concurrent.FutureTask
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.TimeUnit
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

@AutoHandleExceptions
class ViewIndexer(activity: Activity) {
  private val uiThreadHandler: Handler
  private val activityReference: WeakReference<Activity> = WeakReference(activity)
  private var indexingTimer: Timer? = null
  private var previousDigest: String? = null

  fun schedule() {
    val indexingTask: TimerTask =
        object : TimerTask() {
          override fun run() {
            try {
              val activity = activityReference.get()
              val rootView = getRootView(activity)
              if (null == activity || null == rootView) {
                return
              }
              val activityName = activity.javaClass.simpleName
              if (!getIsAppIndexingEnabled()) {
                return
              }
              if (isUnityApp) {
                UnityReflection.captureViewHierarchy()
                return
              }
              val screenshotFuture = FutureTask(ScreenshotTaker(rootView))
              uiThreadHandler.post(screenshotFuture)
              var screenshot: String? = ""
              try {
                screenshot = screenshotFuture[1, TimeUnit.SECONDS]
              } catch (e: Exception) {
                Log.e(TAG, "Failed to take screenshot.", e)
              }
              val viewTree = JSONObject()
              try {
                viewTree.put("screenname", activityName)
                viewTree.put("screenshot", screenshot)
                val viewArray = JSONArray()
                val rootViewInfo = ViewHierarchy.getDictionaryOfView(rootView)
                viewArray.put(rootViewInfo)
                viewTree.put("view", viewArray)
              } catch (e: JSONException) {
                Log.e(TAG, "Failed to create JSONObject")
              }
              val tree = viewTree.toString()
              sendToServer(tree)
            } catch (e: Exception) {
              Log.e(TAG, "UI Component tree indexing failure!", e)
            }
          }
        }
    try {
      getExecutor().execute {
        try {
          indexingTimer?.cancel()
          previousDigest = null
          val timer = Timer()
          timer.scheduleAtFixedRate(
              indexingTask, 0, Constants.APP_INDEXING_SCHEDULE_INTERVAL_MS.toLong())
          indexingTimer = timer
        } catch (e: Exception) {
          Log.e(TAG, "Error scheduling indexing job", e)
        }
      }
    } catch (e: RejectedExecutionException) {
      Log.e(TAG, "Error scheduling indexing job", e)
    }
  }

  fun unschedule() {
    activityReference.get() ?: return
    try {
      indexingTimer?.cancel()

      indexingTimer = null
    } catch (e: Exception) {
      Log.e(TAG, "Error unscheduling indexing job", e)
    }
  }

  private fun sendToServer(tree: String) {
    getExecutor()
        .execute(
            Runnable {
              val currentDigest = md5hash(tree)
              val accessToken = getCurrentAccessToken()
              if (currentDigest != null && currentDigest == previousDigest) {
                return@Runnable
              }
              val request =
                  buildAppIndexingRequest(
                      tree, accessToken, getApplicationId(), Constants.APP_INDEXING)
              processRequest(request, currentDigest)
            })
  }

  /**
   * Process graph request
   *
   * @param request graphRequest to process
   * @param currentDigest
   */
  fun processRequest(request: GraphRequest?, currentDigest: String?) {
    if (request == null) {
      return
    }
    val res = request.executeAndWait()
    try {
      val jsonRes = res.getJSONObject()
      if (jsonRes != null) {
        if ("true" == jsonRes.optString(SUCCESS)) {
          log(LoggingBehavior.APP_EVENTS, TAG, "Successfully send UI component tree to server")
          previousDigest = currentDigest
        }
        if (jsonRes.has(Constants.APP_INDEXING_ENABLED)) {
          val appIndexingEnabled = jsonRes.getBoolean(Constants.APP_INDEXING_ENABLED)
          updateAppIndexing(appIndexingEnabled)
        }
      } else {
        Log.e(TAG, "Error sending UI component tree to Facebook: " + res.error)
      }
    } catch (e: JSONException) {
      Log.e(TAG, "Error decoding server response.", e)
    }
  }

  private class ScreenshotTaker internal constructor(rootView: View) : Callable<String> {
    private val rootView: WeakReference<View>
    override fun call(): String {
      val view = rootView.get()
      if (view == null || view.width == 0 || view.height == 0) {
        return ""
      }
      val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.RGB_565)
      val canvas = Canvas(bitmap)
      view.draw(canvas)
      val outputStream = ByteArrayOutputStream()
      // TODO: T25009391, Support better screenshot image quality by using file attachment.
      bitmap.compress(Bitmap.CompressFormat.JPEG, 10, outputStream)
      return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
    }

    init {
      this.rootView = WeakReference(rootView)
    }
  }

  companion object {
    private val TAG = ViewIndexer::class.java.canonicalName ?: ""
    private const val SUCCESS = "success"
    private const val TREE_PARAM = "tree"
    private const val APP_VERSION_PARAM = "app_version"
    private const val PLATFORM_PARAM = "platform"
    private const val REQUEST_TYPE = "request_type"
    private var instance: ViewIndexer? = null

    /**
     * Build current app index request and process it
     *
     * @param tree current view tree
     */
    @JvmStatic
    fun sendToServerUnityInstance(tree: String) {
      instance?.sendToServer(tree)
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @JvmStatic
    fun buildAppIndexingRequest(
        appIndex: String?,
        accessToken: AccessToken?,
        appId: String?,
        requestType: String
    ): GraphRequest? {
      if (appIndex == null) {
        return null
      }
      val postRequest =
          newPostRequest(
              accessToken, String.format(Locale.US, "%s/app_indexing", appId), null, null)
      var requestParameters = postRequest.parameters
      if (requestParameters == null) {
        requestParameters = Bundle()
      }
      requestParameters.putString(TREE_PARAM, appIndex)
      requestParameters.putString(APP_VERSION_PARAM, getAppVersion())
      requestParameters.putString(PLATFORM_PARAM, Constants.PLATFORM)
      requestParameters.putString(REQUEST_TYPE, requestType)
      if (requestType == Constants.APP_INDEXING) {
        requestParameters.putString(Constants.DEVICE_SESSION_ID, getCurrentDeviceSessionID())
      }
      postRequest.parameters = requestParameters
      postRequest.callback =
          GraphRequest.Callback { log(LoggingBehavior.APP_EVENTS, TAG, "App index sent to FB!") }
      return postRequest
    }
  }

  init {
    previousDigest = null
    uiThreadHandler = Handler(Looper.getMainLooper())
    instance = this
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import kotlin.math.sqrt

@AutoHandleExceptions
internal class ViewIndexingTrigger : SensorEventListener {
  private var onShakeListener: OnShakeListener? = null
  fun setOnShakeListener(listener: OnShakeListener?) {
    onShakeListener = listener
  }

  fun interface OnShakeListener {
    fun onShake()
  }

  override fun onSensorChanged(event: SensorEvent) {
    onShakeListener?.let {
      val x = event.values[0]
      val y = event.values[1]
      val z = event.values[2]
      val gX = (x / SensorManager.GRAVITY_EARTH).toDouble()
      val gY = (y / SensorManager.GRAVITY_EARTH).toDouble()
      val gZ = (z / SensorManager.GRAVITY_EARTH).toDouble()

      // gForce will be close to 1 when there is no movement.
      val gForce = sqrt(gX * gX + gY * gY + gZ * gZ)
      if (gForce > SHAKE_THRESHOLD_GRAVITY) {
        it.onShake()
      }
    }
  }

  override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
    // no op
  }

  companion object {
    private const val SHAKE_THRESHOLD_GRAVITY = 2.3
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless.internal

object Constants {
  const val MAX_TREE_DEPTH = 25
  const val IS_CODELESS_EVENT_KEY = "_is_fb_codeless"
  const val EVENT_MAPPING_PATH_TYPE_KEY = "path_type"
  const val PATH_TYPE_RELATIVE = "relative"
  const val PATH_TYPE_ABSOLUTE = "absolute"
  const val PLATFORM = "android"
  const val APP_INDEXING_SCHEDULE_INTERVAL_MS = 1000
  const val APP_INDEXING_ENABLED = "is_app_indexing_enabled"
  const val DEVICE_SESSION_ID = "device_session_id"
  const val EXTINFO = "extinfo"
  const val APP_INDEXING = "app_indexing"
  const val BUTTON_SAMPLING = "button_sampling"
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless.internal

import java.util.Collections
import java.util.Locale
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

class EventBinding(
    val eventName: String,
    val method: MappingMethod,
    val type: ActionType,
    val appVersion: String,
    private val path: List<PathComponent>,
    private val parameters: List<ParameterComponent>,
    val componentId: String,
    val pathType: String,
    val activityName: String
) {
  val viewPath: List<PathComponent>
    get() = Collections.unmodifiableList(path)
  val viewParameters: List<ParameterComponent>
    get() = Collections.unmodifiableList(parameters)

  enum class MappingMethod {
    MANUAL,
    INFERENCE
  }

  enum class ActionType {
    CLICK,
    SELECTED,
    TEXT_CHANGED
  }

  companion object {
    @JvmStatic
    fun parseArray(array: JSONArray?): List<EventBinding> {
      val eventBindings = arrayListOf<EventBinding>()
      if (array != null) {
        try {
          for (i in 0 until array.length()) {
            val eventBinding = getInstanceFromJson(array.getJSONObject(i))
            eventBindings.add(eventBinding)
          }
        } catch (e: JSONException) {
          // Ignore
        } catch (e: IllegalArgumentException) {
          // Ignore
        }
      }
      return eventBindings
    }

    @Throws(JSONException::class, IllegalArgumentException::class)
    @JvmStatic
    fun getInstanceFromJson(mapping: JSONObject): EventBinding {
      val eventName = mapping.getString("event_name")
      val method = MappingMethod.valueOf(mapping.getString("method").uppercase(Locale.ENGLISH))
      val type = ActionType.valueOf(mapping.getString("event_type").uppercase(Locale.ENGLISH))
      val appVersion = mapping.getString("app_version")
      val jsonPathArray = mapping.getJSONArray("path")
      val path = arrayListOf<PathComponent>()
      for (i in 0 until jsonPathArray.length()) {
        val jsonPath = jsonPathArray.getJSONObject(i)
        val component = PathComponent(jsonPath)
        path.add(component)
      }
      val pathType =
          mapping.optString(Constants.EVENT_MAPPING_PATH_TYPE_KEY, Constants.PATH_TYPE_ABSOLUTE)
      val jsonParameterArray = mapping.optJSONArray("parameters")
      val parameters = arrayListOf<ParameterComponent>()
      if (null != jsonParameterArray) {
        for (i in 0 until jsonParameterArray.length()) {
          val jsonParameter = jsonParameterArray.getJSONObject(i)
          val component = ParameterComponent(jsonParameter)
          parameters.add(component)
        }
      }
      val componentId = mapping.optString("component_id")
      val activityName = mapping.optString("activity_name")
      return EventBinding(
          eventName,
          method,
          type,
          appVersion,
          path,
          parameters,
          componentId,
          pathType,
          activityName)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless.internal

import org.json.JSONObject

class ParameterComponent(component: JSONObject) {
  val name: String = component.getString(PARAMETER_NAME_KEY)
  val value: String = component.optString(PARAMETER_VALUE_KEY)
  val path: List<PathComponent>
  val pathType: String =
      component.optString(Constants.EVENT_MAPPING_PATH_TYPE_KEY, Constants.PATH_TYPE_ABSOLUTE)

  companion object {
    private const val PARAMETER_NAME_KEY = "name"
    private const val PARAMETER_PATH_KEY = "path"
    private const val PARAMETER_VALUE_KEY = "value"
  }

  init {
    val pathComponents = arrayListOf<PathComponent>()
    val jsonPathArray = component.optJSONArray(PARAMETER_PATH_KEY)
    if (jsonPathArray != null) {
      for (i in 0 until jsonPathArray.length()) {
        val pathComponent = PathComponent(jsonPathArray.getJSONObject(i))
        pathComponents.add(pathComponent)
      }
    }
    path = pathComponents
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless.internal

import org.json.JSONObject

class PathComponent internal constructor(component: JSONObject) {
  enum class MatchBitmaskType(val value: Int) {
    ID(1),
    TEXT(1 shl 1),
    TAG(1 shl 2),
    DESCRIPTION(1 shl 3),
    HINT(1 shl 4)
  }

  val className: String = component.getString(PATH_CLASS_NAME_KEY)
  val index: Int = component.optInt(PATH_INDEX_KEY, -1)
  val id: Int = component.optInt(PATH_ID_KEY)
  val text: String = component.optString(PATH_TEXT_KEY)
  val tag: String = component.optString(PATH_TAG_KEY)
  val description: String = component.optString(PATH_DESCRIPTION_KEY)
  val hint: String = component.optString(PATH_HINT_KEY)
  val matchBitmask: Int = component.optInt(PATH_MATCH_BITMASK_KEY)

  companion object {
    private const val PATH_CLASS_NAME_KEY = "class_name"
    private const val PATH_INDEX_KEY = "index"
    private const val PATH_ID_KEY = "id"
    private const val PATH_TEXT_KEY = "text"
    private const val PATH_TAG_KEY = "tag"
    private const val PATH_DESCRIPTION_KEY = "description"
    private const val PATH_HINT_KEY = "hint"
    private const val PATH_MATCH_BITMASK_KEY = "match_bitmask"
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless.internal

import android.text.InputType
import android.text.method.PasswordTransformationMethod
import android.util.Patterns
import android.view.View
import android.widget.TextView
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions

@AutoHandleExceptions
object SensitiveUserDataUtils {
  @JvmStatic
  fun isSensitiveUserData(view: View?): Boolean {
    if (view is TextView) {
      return (isPassword(view) ||
          isCreditCard(view) ||
          isPersonName(view) ||
          isPostalAddress(view) ||
          isPhoneNumber(view) ||
          isEmail(view))
    }
    return false
  }

  private fun isPassword(view: TextView): Boolean {
    val inputType = view.inputType
    if (inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD) {
      return true
    }
    val method = view.transformationMethod
    return method is PasswordTransformationMethod
  }

  private fun isEmail(view: TextView): Boolean {
    val inputType = view.inputType
    if (inputType == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) {
      return true
    }
    val text = ViewHierarchy.getTextOfView(view)
    return if (text == null || text.isEmpty()) {
      false
    } else Patterns.EMAIL_ADDRESS.matcher(text).matches()
  }

  private fun isPersonName(view: TextView): Boolean {
    val inputType = view.inputType
    return inputType == InputType.TYPE_TEXT_VARIATION_PERSON_NAME
  }

  private fun isPostalAddress(view: TextView): Boolean {
    val inputType = view.inputType
    return inputType == InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
  }

  private fun isPhoneNumber(view: TextView): Boolean {
    val inputType = view.inputType
    return inputType == InputType.TYPE_CLASS_PHONE
  }

  private fun isCreditCard(view: TextView): Boolean {
    val ccNumber = ViewHierarchy.getTextOfView(view).replace("\\s".toRegex(), "")
    val length = ccNumber.length
    if (length < 12 || length > 19) {
      return false
    }
    var sum = 0
    var alternate = false
    for (i in length - 1 downTo 0) {
      val digit = ccNumber[i]
      if (!digit.isDigit()) {
        return false
      }
      var n = digit.digitToInt()
      if (alternate) {
        n *= 2
        if (n > 9) {
          n = n % 10 + 1
        }
      }
      sum += n
      alternate = !alternate
    }
    return sum % 10 == 0
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.appevents.codeless.internal

import android.util.Log

object UnityReflection {
  private val TAG = UnityReflection::class.java.canonicalName
  private const val UNITY_PLAYER_CLASS = "com.unity3d.player.UnityPlayer"
  private const val UNITY_SEND_MESSAGE_METHOD = "UnitySendMessage"
  private const val FB_UNITY_GAME_OBJECT = "UnityFacebookSDKPlugin"
  private const val CAPTURE_VIEW_HIERARCHY_METHOD = "CaptureViewHierarchy"
  private const val EVENT_MAPPING_METHOD = "OnReceiveMapping"
  private lateinit var unityPlayer: Class<*>

  private fun getUnityPlayerClass(): Class<*> = Class.forName(UNITY_PLAYER_CLASS)

  @JvmStatic
  fun sendMessage(unityObject: String?, unityMethod: String?, message: String?) {
    try {
      if (!::unityPlayer.isInitialized) {
        unityPlayer = getUnityPlayerClass()
      }
      val method =
          unityPlayer.getMethod(
              UNITY_SEND_MESSAGE_METHOD, String::class.java, String::class.java, String::class.java)
      method.invoke(unityPlayer, unityObject, unityMethod, message)
    } catch (e: Exception) {
      Log.e(TAG, "Failed to send message to Unity", e)
    }
  }

  @JvmStatic
  fun captureViewHierarchy() {
    sendMessage(FB_UNITY_GAME_OBJECT, CAPTURE_VIEW_HIERARCHY_METHOD, "")
  }

  @JvmStatic
  fun sendEventMapping(eventMapping: String?) {
    sendMessage(FB_UNITY_GAME_OBJECT, EVENT_MAPPING_METHOD, eventMapping)
  }
}