// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/safe_browsing/android/safe_browsing_api_handler_bridge.h"

#include <memory>
#include <string>
#include <utility>

#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/callback_list.h"
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/containers/heap_array.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/string_tokenizer.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/types/fixed_array.h"
#include "components/safe_browsing/android/safe_browsing_api_handler_util.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/safebrowsing_switches.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/safe_browsing/android/jni_headers/SafeBrowsingApiBridge_jni.h"

using base::android::AttachCurrentThread;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaIntArrayToIntVector;
using base::android::JavaRef;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaIntArray;
using content::BrowserThread;

namespace safe_browsing {

namespace {

std::string GetSafeBrowsingJavaProtocolUmaSuffix(
    SafeBrowsingJavaProtocol protocol) {
  switch (protocol) {
    case SafeBrowsingJavaProtocol::LOCAL_BLOCK_LIST:
      return ".LocalBlocklist";
    case SafeBrowsingJavaProtocol::REAL_TIME:
      return ".RealTime";
  }
}

void ReportSafeBrowsingJavaValidationResult(
    SafeBrowsingJavaProtocol protocol,
    SafeBrowsingJavaValidationResult validation_result) {
  base::UmaHistogramEnumeration(
      "SafeBrowsing.GmsSafeBrowsingApi.JavaValidationResult",
      validation_result);
  base::UmaHistogramEnumeration(
      "SafeBrowsing.GmsSafeBrowsingApi.JavaValidationResult" +
          GetSafeBrowsingJavaProtocolUmaSuffix(protocol),
      validation_result);
}

void ReportUmaHistogramSparseWithAndWithoutSuffix(const std::string& metric,
                                                  const std::string& suffix,
                                                  int value) {
  base::UmaHistogramSparse(metric, value);
  base::UmaHistogramSparse(metric + suffix, value);
}

void ReportSafeBrowsingJavaResponse(
    SafeBrowsingJavaProtocol protocol,
    SafeBrowsingApiLookupResult lookup_result,
    SafeBrowsingJavaThreatType threat_type,
    const std::vector<int>& threat_attributes,
    SafeBrowsingJavaResponseStatus response_status,
    int64_t check_delta_microseconds) {
  std::string suffix = GetSafeBrowsingJavaProtocolUmaSuffix(protocol);

  base::UmaHistogramMicrosecondsTimes(
      "SafeBrowsing.GmsSafeBrowsingApi.CheckDelta",
      base::Microseconds(check_delta_microseconds));
  base::UmaHistogramMicrosecondsTimes(
      "SafeBrowsing.GmsSafeBrowsingApi.CheckDelta" + suffix,
      base::Microseconds(check_delta_microseconds));

  ReportUmaHistogramSparseWithAndWithoutSuffix(
      "SafeBrowsing.GmsSafeBrowsingApi.LookupResult", suffix,
      static_cast<int>(lookup_result));
  if (lookup_result != SafeBrowsingApiLookupResult::SUCCESS) {
    // Do not log other histograms if the lookup failed, since the other values
    // will all be dummy values.
    return;
  }
  ReportUmaHistogramSparseWithAndWithoutSuffix(
      "SafeBrowsing.GmsSafeBrowsingApi.ThreatType2", suffix,
      static_cast<int>(threat_type));
  base::UmaHistogramCounts100(
      "SafeBrowsing.GmsSafeBrowsingApi.ThreatAttributeCount",
      threat_attributes.size());
  base::UmaHistogramCounts100(
      "SafeBrowsing.GmsSafeBrowsingApi.ThreatAttributeCount" + suffix,
      threat_attributes.size());
  for (int threat_attribute : threat_attributes) {
    ReportUmaHistogramSparseWithAndWithoutSuffix(
        "SafeBrowsing.GmsSafeBrowsingApi.ThreatAttribute", suffix,
        threat_attribute);
  }
  ReportUmaHistogramSparseWithAndWithoutSuffix(
      "SafeBrowsing.GmsSafeBrowsingApi.ResponseStatus", suffix,
      static_cast<int>(response_status));
}

SafeBrowsingJavaValidationResult GetJavaValidationResult(
    SafeBrowsingApiLookupResult lookup_result,
    SafeBrowsingJavaThreatType threat_type,
    const std::vector<int>& threat_attributes,
    SafeBrowsingJavaResponseStatus response_status) {
  bool is_lookup_result_recognized = false;
  switch (lookup_result) {
    case SafeBrowsingApiLookupResult::SUCCESS:
    case SafeBrowsingApiLookupResult::FAILURE:
    case SafeBrowsingApiLookupResult::FAILURE_API_CALL_TIMEOUT:
    case SafeBrowsingApiLookupResult::FAILURE_API_UNSUPPORTED:
    case SafeBrowsingApiLookupResult::FAILURE_API_NOT_AVAILABLE:
    case SafeBrowsingApiLookupResult::FAILURE_HANDLER_NULL:
      is_lookup_result_recognized = true;
      break;
  }
  if (!is_lookup_result_recognized) {
    return SafeBrowsingJavaValidationResult::INVALID_LOOKUP_RESULT;
  }

  bool is_threat_type_recognized = false;
  switch (threat_type) {
    case SafeBrowsingJavaThreatType::NO_THREAT:
    case SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING:
    case SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE:
    case SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
    case SafeBrowsingJavaThreatType::BILLING:
    case SafeBrowsingJavaThreatType::ABUSIVE_EXPERIENCE_VIOLATION:
    case SafeBrowsingJavaThreatType::BETTER_ADS_VIOLATION:
      is_threat_type_recognized = true;
      break;
  }
  if (!is_threat_type_recognized) {
    return SafeBrowsingJavaValidationResult::INVALID_THREAT_TYPE;
  }

  for (int threat_attribute : threat_attributes) {
    SafeBrowsingJavaThreatAttribute threat_attribute_enum =
        static_cast<SafeBrowsingJavaThreatAttribute>(threat_attribute);
    bool is_threat_attribute_recognized = false;
    switch (threat_attribute_enum) {
      case SafeBrowsingJavaThreatAttribute::CANARY:
      case SafeBrowsingJavaThreatAttribute::FRAME_ONLY:
        is_threat_attribute_recognized = true;
        break;
    }
    if (!is_threat_attribute_recognized) {
      return SafeBrowsingJavaValidationResult::INVALID_THREAT_ATTRIBUTE;
    }
  }

  bool is_reponse_status_recognized = false;
  switch (response_status) {
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_LOCAL_BLOCKLIST:
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_REAL_TIME:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_TIMEOUT:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_THROTTLED:
    case SafeBrowsingJavaResponseStatus::FAILURE_NETWORK_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_BLOCK_LIST_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_INVALID_URL:
      is_reponse_status_recognized = true;
      break;
  }
  if (!is_reponse_status_recognized) {
    return SafeBrowsingJavaValidationResult::
        VALID_WITH_UNRECOGNIZED_RESPONSE_STATUS;
  }

  return SafeBrowsingJavaValidationResult::VALID;
}

// Validate the values returned from SafeBrowsing API are defined in enum. The
// response can be out of range if there is version mismatch between Chrome and
// the GMSCore APK, or the enums between c++ and java are not aligned.
bool IsResponseFromJavaValid(SafeBrowsingJavaProtocol protocol,
                             SafeBrowsingApiLookupResult lookup_result,
                             SafeBrowsingJavaThreatType threat_type,
                             const std::vector<int>& threat_attributes,
                             SafeBrowsingJavaResponseStatus response_status) {
  SafeBrowsingJavaValidationResult validation_result = GetJavaValidationResult(
      lookup_result, threat_type, threat_attributes, response_status);
  ReportSafeBrowsingJavaValidationResult(protocol, validation_result);

  switch (validation_result) {
    case SafeBrowsingJavaValidationResult::VALID:
    // Not returning false if response_status is unrecognized. This is to avoid
    // the API adding a new success response_status while we haven't integrated
    // the new value yet. In this case, we still want to return the threat_type.
    case SafeBrowsingJavaValidationResult::
        VALID_WITH_UNRECOGNIZED_RESPONSE_STATUS:
      return true;
    case SafeBrowsingJavaValidationResult::INVALID_LOOKUP_RESULT:
    case SafeBrowsingJavaValidationResult::INVALID_THREAT_TYPE:
    case SafeBrowsingJavaValidationResult::INVALID_THREAT_ATTRIBUTE:
      return false;
  }
}

bool IsLookupSuccessful(SafeBrowsingApiLookupResult lookup_result,
                        SafeBrowsingJavaResponseStatus response_status) {
  bool is_lookup_result_success = false;
  switch (lookup_result) {
    case SafeBrowsingApiLookupResult::SUCCESS:
      is_lookup_result_success = true;
      break;
    case SafeBrowsingApiLookupResult::FAILURE:
    case SafeBrowsingApiLookupResult::FAILURE_API_CALL_TIMEOUT:
    case SafeBrowsingApiLookupResult::FAILURE_API_UNSUPPORTED:
    case SafeBrowsingApiLookupResult::FAILURE_API_NOT_AVAILABLE:
    case SafeBrowsingApiLookupResult::FAILURE_HANDLER_NULL:
      break;
  }
  if (!is_lookup_result_success) {
    return false;
  }

  // Note that we check explicit failure statuses instead of success statuses.
  // This is to avoid the API adding a new success response_status while we
  // haven't integrated the new value yet. The impact of a missing failure
  // status is smaller since the API is expected to return a safe threat type in
  // a failure anyway.
  bool is_response_status_success = true;
  switch (response_status) {
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_LOCAL_BLOCKLIST:
    case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_REAL_TIME:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_TIMEOUT:
    case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_THROTTLED:
      break;
    case SafeBrowsingJavaResponseStatus::FAILURE_NETWORK_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_BLOCK_LIST_UNAVAILABLE:
    case SafeBrowsingJavaResponseStatus::FAILURE_INVALID_URL:
      is_response_status_success = false;
      break;
  }
  return is_response_status_success;
}

bool IsSafeBrowsingNonRecoverable(SafeBrowsingApiLookupResult lookup_result) {
  switch (lookup_result) {
    case SafeBrowsingApiLookupResult::FAILURE_API_UNSUPPORTED:
    case SafeBrowsingApiLookupResult::FAILURE_API_NOT_AVAILABLE:
    case SafeBrowsingApiLookupResult::FAILURE_HANDLER_NULL:
      return true;
    case SafeBrowsingApiLookupResult::SUCCESS:
    case SafeBrowsingApiLookupResult::FAILURE:
    case SafeBrowsingApiLookupResult::FAILURE_API_CALL_TIMEOUT:
      return false;
  }
}

// Convert a SBThreatType to a Java SafetyNet API threat type.  We only support
// CSD_ALLOWLIST.
SafetyNetJavaThreatType SBThreatTypeToSafetyNetJavaThreatType(
    const SBThreatType& sb_threat_type) {
  using enum SBThreatType;
  CHECK(sb_threat_type == SB_THREAT_TYPE_CSD_ALLOWLIST);
  return SafetyNetJavaThreatType::CSD_ALLOWLIST;
}

// Convert a Java threat type for SafeBrowsing to a SBThreatType.
SBThreatType SafeBrowsingJavaToSBThreatType(
    SafeBrowsingJavaThreatType java_threat_num) {
  using enum SBThreatType;
  switch (java_threat_num) {
    case SafeBrowsingJavaThreatType::NO_THREAT:
      return SB_THREAT_TYPE_SAFE;
    case SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING:
      return SB_THREAT_TYPE_URL_PHISHING;
    case SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE:
      return SB_THREAT_TYPE_URL_UNWANTED;
    case SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
      return SB_THREAT_TYPE_URL_MALWARE;
    case SafeBrowsingJavaThreatType::BILLING:
      return SB_THREAT_TYPE_BILLING;
    case SafeBrowsingJavaThreatType::ABUSIVE_EXPERIENCE_VIOLATION:
    case SafeBrowsingJavaThreatType::BETTER_ADS_VIOLATION:
      return SB_THREAT_TYPE_SUBRESOURCE_FILTER;
  }
}

// Convert a SBThreatType to a Java threat type for SafeBrowsing. We only
// support a few.
SafeBrowsingJavaThreatType SBThreatTypeToSafeBrowsingApiJavaThreatType(
    const SBThreatType& sb_threat_type) {
  using enum SBThreatType;
  switch (sb_threat_type) {
    case SB_THREAT_TYPE_URL_PHISHING:
      return SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING;
    case SB_THREAT_TYPE_URL_UNWANTED:
      return SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE;
    case SB_THREAT_TYPE_URL_MALWARE:
      return SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION;
    case SB_THREAT_TYPE_BILLING:
      return SafeBrowsingJavaThreatType::BILLING;
    case SB_THREAT_TYPE_UNUSED:
    case SB_THREAT_TYPE_SAFE:
    case SB_THREAT_TYPE_URL_BINARY_MALWARE:
    case SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING:
    case SB_THREAT_TYPE_EXTENSION:
    case DEPRECATED_SB_THREAT_TYPE_URL_CLIENT_SIDE_MALWARE:
    case SB_THREAT_TYPE_API_ABUSE:
    case SB_THREAT_TYPE_SUBRESOURCE_FILTER:
    case SB_THREAT_TYPE_CSD_ALLOWLIST:
    case DEPRECATED_SB_THREAT_TYPE_URL_PASSWORD_PROTECTION_PHISHING:
    case SB_THREAT_TYPE_SAVED_PASSWORD_REUSE:
    case SB_THREAT_TYPE_SIGNED_IN_SYNC_PASSWORD_REUSE:
    case SB_THREAT_TYPE_SIGNED_IN_NON_SYNC_PASSWORD_REUSE:
    case SB_THREAT_TYPE_BLOCKED_AD_REDIRECT:
    case SB_THREAT_TYPE_AD_SAMPLE:
    case SB_THREAT_TYPE_BLOCKED_AD_POPUP:
    case SB_THREAT_TYPE_SUSPICIOUS_SITE:
    case SB_THREAT_TYPE_ENTERPRISE_PASSWORD_REUSE:
    case SB_THREAT_TYPE_APK_DOWNLOAD:
    case SB_THREAT_TYPE_HIGH_CONFIDENCE_ALLOWLIST:
    case SB_THREAT_TYPE_CSD_DOWNLOAD_ALLOWLIST:
    case SB_THREAT_TYPE_MANAGED_POLICY_WARN:
    case SB_THREAT_TYPE_MANAGED_POLICY_BLOCK:
    case SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE:
      NOTREACHED();
  }
}

// Convert a vector of SBThreatTypes to JavaIntArray of SafeBrowsing API's
// threat types.
ScopedJavaLocalRef<jintArray> SBThreatTypeSetToSafeBrowsingJavaArray(
    JNIEnv* env,
    const SBThreatTypeSet& threat_types) {
  DCHECK_LT(0u, threat_types.size());
  size_t threat_type_size =
      threat_types.contains(SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER)
          ? threat_types.size() + 1
          : threat_types.size();
  auto int_threat_types = base::HeapArray<int>::WithSize(threat_type_size);
  auto itr = int_threat_types.begin();
  for (auto threat_type : threat_types) {
    if (threat_type == SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER) {
      *itr++ = static_cast<int>(
          SafeBrowsingJavaThreatType::ABUSIVE_EXPERIENCE_VIOLATION);
      *itr++ =
          static_cast<int>(SafeBrowsingJavaThreatType::BETTER_ADS_VIOLATION);
    } else {
      *itr++ = static_cast<int>(
          SBThreatTypeToSafeBrowsingApiJavaThreatType(threat_type));
    }
  }
  return ToJavaIntArray(env, int_threat_types);
}

// Customized struct to hold a callback to the SafeBrowsing API and the protocol
// used to make that call. The protocol is stored for histogram logging.
struct SafeBrowsingResponseCallback {
  SafeBrowsingJavaProtocol protocol;
  SafeBrowsingApiHandlerBridge::ResponseCallback response_callback;
};

// The map that holds the callback_id used to reference each pending
// SafeBrowsing request sent to Java, and the corresponding callback to call on
// receiving the response.
using PendingSafeBrowsingCallbacksMap =
    std::unordered_map<int64_t, SafeBrowsingResponseCallback>;

PendingSafeBrowsingCallbacksMap& GetPendingSafeBrowsingCallbacksMap() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Holds the list of callback objects that we are currently waiting to hear
  // the result of from GmsCore.
  // The key is a unique count-up integer.
  static base::NoDestructor<PendingSafeBrowsingCallbacksMap>
      pending_safe_browsing_callbacks;
  return *pending_safe_browsing_callbacks;
}

using PendingVerifyAppsCallbacksMap = std::unordered_map<
    int64_t,
    SafeBrowsingApiHandlerBridge::VerifyAppsResponseCallback>;
PendingVerifyAppsCallbacksMap& GetPendingVerifyAppsCallbacks() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Holds the list of callback objects that we are currently waiting to hear
  // the result of from GmsCore.
  // The key is a unique count-up integer.
  static base::NoDestructor<PendingVerifyAppsCallbacksMap> pending_callbacks;
  return *pending_callbacks;
}

using PendingHarmfulAppsCallbacksMap = std::unordered_map<
    int64_t,
    SafeBrowsingApiHandlerBridge::HasHarmfulAppsResponseCallback>;
PendingHarmfulAppsCallbacksMap& GetPendingHarmfulAppsCallbacks() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Holds the list of callback objects that we are currently waiting to hear
  // the result of from GmsCore.
  // The key is a unique count-up integer.
  static base::NoDestructor<PendingHarmfulAppsCallbacksMap>
      pending_harmful_app_callbacks;
  return *pending_harmful_app_callbacks;
}

// Holds the list of callback objects that we are currently waiting to hear
// the result of getSafetyNetId() from GmsCore.
using PendingGetSafetyNetIdCallbacksList =
    base::OnceCallbackList<void(const std::string&)>;
PendingGetSafetyNetIdCallbacksList& GetPendingGetSafetyNetIdCallbacks() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  static base::NoDestructor<PendingGetSafetyNetIdCallbacksList>
      pending_get_safety_net_id_callbacks;
  return *pending_get_safety_net_id_callbacks;
}

bool StartAllowlistCheck(const GURL& url, SafetyNetJavaThreatType threat_type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    return false;
  }

  ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
  int j_threat_type = static_cast<int>(threat_type);
  return Java_SafeBrowsingApiBridge_startAllowlistLookup(env, j_url,
                                                         j_threat_type);
}

}  // namespace

// static
SafeBrowsingApiHandlerBridge& SafeBrowsingApiHandlerBridge::GetInstance() {
  static base::NoDestructor<SafeBrowsingApiHandlerBridge> instance;
  return *instance.get();
}

// Respond to the URL reputation request by looking up the callback information
// stored in |pending_safe_browsing_callbacks|. Must be called on the original
// thread that starts the lookup.
void OnUrlCheckDoneBySafeBrowsingApi(
    int64_t callback_id,
    SafeBrowsingApiLookupResult lookup_result,
    SafeBrowsingJavaThreatType threat_type,
    std::vector<int> threat_attributes,
    SafeBrowsingJavaResponseStatus response_status,
    int64_t check_delta_microseconds) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  PendingSafeBrowsingCallbacksMap& pending_callbacks =
      GetPendingSafeBrowsingCallbacksMap();
  bool found = pending_callbacks.contains(callback_id);
  DCHECK(found) << "Not found in pending_safe_browsing_callbacks: "
                << callback_id;
  if (!found) {
    return;
  }

  SafeBrowsingResponseCallback callback =
      std::move(pending_callbacks[callback_id]);
  pending_callbacks.erase(callback_id);

  ReportSafeBrowsingJavaResponse(callback.protocol, lookup_result, threat_type,
                                 threat_attributes, response_status,
                                 check_delta_microseconds);

  if (!IsResponseFromJavaValid(callback.protocol, lookup_result, threat_type,
                               threat_attributes, response_status)) {
    std::move(callback.response_callback)
        .Run(SBThreatType::SB_THREAT_TYPE_SAFE, ThreatMetadata());
    return;
  }

  if (!IsLookupSuccessful(lookup_result, response_status)) {
    if (IsSafeBrowsingNonRecoverable(lookup_result)) {
      SafeBrowsingApiHandlerBridge::GetInstance()
          .OnSafeBrowsingApiNonRecoverableFailure();
    }
    std::move(callback.response_callback)
        .Run(SBThreatType::SB_THREAT_TYPE_SAFE, ThreatMetadata());
    return;
  }

  std::move(callback.response_callback)
      .Run(
          SafeBrowsingJavaToSBThreatType(threat_type),
          GetThreatMetadataFromSafeBrowsingApi(threat_type, threat_attributes));
}

// Java->Native call, invoked when a SafeBrowsing check is done. |env| is the
// JNI environment that stores local pointers. |callback_id| is a key into the
// |pending_safe_browsing_callbacks| map, whose value is a ::ResponseCallback
// that will be called and then deleted on the IO thread. |j_lookup_result| is a
// @LookupResult from SafeBrowsingApiHandler.java. |j_threat_type| is the threat
// type that matched against the URL. |j_threat_attributes| is the threat
// attributes that matched against the URL. |j_response_status| reflects how the
// API gets the response. |check_delta_microseconds| is the number of
// microseconds it took to look up the URL reputation from GmsCore.
//
// Careful note: this can be called on multiple threads, so make sure there is
// nothing thread unsafe happening here.
static void JNI_SafeBrowsingApiBridge_OnUrlCheckDoneBySafeBrowsingApi(
    JNIEnv* env,
    int64_t callback_id,
    int32_t j_lookup_result,
    int32_t j_threat_type,
    const JavaRef<jintArray>& j_threat_attributes,
    int32_t j_response_status,
    int64_t check_delta_microseconds) {
  SafeBrowsingApiLookupResult lookup_result =
      static_cast<SafeBrowsingApiLookupResult>(j_lookup_result);
  SafeBrowsingJavaThreatType threat_type =
      static_cast<SafeBrowsingJavaThreatType>(j_threat_type);
  std::vector<int> threat_attributes;
  JavaIntArrayToIntVector(env, j_threat_attributes, &threat_attributes);
  SafeBrowsingJavaResponseStatus response_status =
      static_cast<SafeBrowsingJavaResponseStatus>(j_response_status);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&OnUrlCheckDoneBySafeBrowsingApi, callback_id,
                     lookup_result, threat_type, std::move(threat_attributes),
                     response_status, check_delta_microseconds));
}

void OnVerifyAppsEnabledDone(int64_t callback_id, int32_t j_result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  PendingVerifyAppsCallbacksMap& pending_callbacks =
      GetPendingVerifyAppsCallbacks();
  bool found = pending_callbacks.contains(callback_id);
  DCHECK(found) << "Not found in pending_verify_apps_callbacks: "
                << callback_id;
  if (!found) {
    return;
  }

  SafeBrowsingApiHandlerBridge::VerifyAppsResponseCallback callback =
      std::move(pending_callbacks[callback_id]);
  std::move(callback).Run(static_cast<VerifyAppsEnabledResult>(j_result));
}

static void JNI_SafeBrowsingApiBridge_OnVerifyAppsEnabledDone(
    JNIEnv* env,
    int64_t callback_id,
    int32_t j_result) {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&OnVerifyAppsEnabledDone, callback_id, j_result));
}

void OnHasHarmfulAppsDone(int64_t callback_id,
                          int32_t j_result,
                          int32_t j_num_of_apps,
                          int32_t j_status_code) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  PendingHarmfulAppsCallbacksMap& pending_callbacks =
      GetPendingHarmfulAppsCallbacks();
  bool found = pending_callbacks.contains(callback_id);
  DCHECK(found) << "Not found in pending_harmful_apps_callbacks: "
                << callback_id;
  if (!found) {
    return;
  }

  SafeBrowsingApiHandlerBridge::HasHarmfulAppsResponseCallback callback =
      std::move(pending_callbacks[callback_id]);
  std::move(callback).Run(static_cast<HasHarmfulAppsResultStatus>(j_result),
                          static_cast<int>(j_num_of_apps),
                          static_cast<int>(j_status_code));
}

static void JNI_SafeBrowsingApiBridge_OnHasHarmfulAppsDone(
    JNIEnv* env,
    int64_t callback_id,
    int32_t j_result,
    int32_t j_num_of_apps,
    int32_t j_status_code) {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&OnHasHarmfulAppsDone, callback_id, j_result,
                                j_num_of_apps, j_status_code));
}

void OnGetSafetyNetIdDone(const std::string& result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  GetPendingGetSafetyNetIdCallbacks().Notify(result);
}

static void JNI_SafeBrowsingApiBridge_OnGetSafetyNetIdDone(
    JNIEnv* env,
    const jni_zero::JavaRef<jstring>& result) {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&OnGetSafetyNetIdDone,
                                base::android::ConvertJavaStringToUTF8(
                                    env, result.obj())));
}

//
// SafeBrowsingApiHandlerBridge
//
SafeBrowsingApiHandlerBridge::SafeBrowsingApiHandlerBridge() = default;
SafeBrowsingApiHandlerBridge::~SafeBrowsingApiHandlerBridge() = default;

void SafeBrowsingApiHandlerBridge::ClearArtificialDatabase() {
  artificially_marked_phishing_urls_.clear();
}

void SafeBrowsingApiHandlerBridge::PopulateArtificialDatabase() {
  const std::string raw_artificial_urls =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kMarkAsPhishing);
  base::StringTokenizer tokenizer(raw_artificial_urls, ",");
  while (tokenizer.GetNext()) {
    auto candidate_url = GURL(tokenizer.token_piece());
    if (candidate_url.is_valid()) {
      artificially_marked_phishing_urls_.insert(candidate_url);
    }
  }
}

void SafeBrowsingApiHandlerBridge::StartHashDatabaseUrlCheck(
    ResponseCallback callback,
    const GURL& url,
    const SBThreatTypeSet& threat_types) {
  bool for_browse_url = SBThreatTypeSetIsValidForCheckBrowseUrl(threat_types);
  if (for_browse_url &&
      threat_types.contains(SBThreatType::SB_THREAT_TYPE_URL_PHISHING) &&
      artificially_marked_phishing_urls_.contains(url)) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback),
                                  SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
                                  ThreatMetadata()));
    return;
  }
  StartUrlCheckBySafeBrowsing(std::move(callback), url, threat_types,
                              SafeBrowsingJavaProtocol::LOCAL_BLOCK_LIST);
}

void SafeBrowsingApiHandlerBridge::StartHashRealTimeUrlCheck(
    ResponseCallback callback,
    const GURL& url,
    const SBThreatTypeSet& threat_types) {
  StartUrlCheckBySafeBrowsing(std::move(callback), url, threat_types,
                              SafeBrowsingJavaProtocol::REAL_TIME);
}

void SafeBrowsingApiHandlerBridge::StartUrlCheckBySafeBrowsing(
    ResponseCallback callback,
    const GURL& url,
    const SBThreatTypeSet& threat_types,
    const SafeBrowsingJavaProtocol& protocol) {
  if (interceptor_for_testing_) {
    // For testing, only check the interceptor.
    interceptor_for_testing_->CheckBySafeBrowsing(std::move(callback), url);
    return;
  }
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  base::UmaHistogramBoolean("SafeBrowsing.GmsSafeBrowsingApi.IsAvailable",
                            is_safe_browsing_api_available_);
  base::UmaHistogramBoolean("SafeBrowsing.GmsSafeBrowsingApi.IsAvailable" +
                                GetSafeBrowsingJavaProtocolUmaSuffix(protocol),
                            is_safe_browsing_api_available_);

  if (!is_safe_browsing_api_available_) {
    // Mark all requests as safe. Only users who have an old, broken GMSCore or
    // have sideloaded Chrome w/o PlayStore should land here.
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), SBThreatType::SB_THREAT_TYPE_SAFE,
                       ThreatMetadata()));
    return;
  }

  JNIEnv* env = AttachCurrentThread();
  int64_t callback_id = next_safe_browsing_callback_id_++;
  auto safe_browsing_callback =
      SafeBrowsingResponseCallback(protocol, std::move(callback));
  GetPendingSafeBrowsingCallbacksMap().insert(
      {callback_id, std::move(safe_browsing_callback)});

  DCHECK(!threat_types.empty());

  ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
  ScopedJavaLocalRef<jintArray> j_threat_types =
      SBThreatTypeSetToSafeBrowsingJavaArray(env, threat_types);
  int j_int_protocol = static_cast<int>(protocol);

  Java_SafeBrowsingApiBridge_startUriLookupBySafeBrowsingApi(
      env, callback_id, j_url, j_threat_types, j_int_protocol);
}

bool SafeBrowsingApiHandlerBridge::StartCSDAllowlistCheck(const GURL& url) {
  return StartAllowlistCheck(
      url, SBThreatTypeToSafetyNetJavaThreatType(
               safe_browsing::SBThreatType::SB_THREAT_TYPE_CSD_ALLOWLIST));
}

bool SafeBrowsingApiHandlerBridge::StartCSDDownloadAllowlistCheck(
    const GURL& url) {
  return StartAllowlistCheck(url,
                             SafetyNetJavaThreatType::CSD_DOWNLOAD_ALLOWLIST);
}

void SafeBrowsingApiHandlerBridge::StartIsVerifyAppsEnabled(
    VerifyAppsResponseCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (verify_apps_enabled_for_testing_.has_value()) {
    std::move(callback).Run(verify_apps_enabled_for_testing_.value());
    return;
  }

  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    std::move(callback).Run(VerifyAppsEnabledResult::FAILED);
    return;
  }

  int64_t callback_id = next_verify_apps_callback_id_++;
  GetPendingVerifyAppsCallbacks().insert({callback_id, std::move(callback)});
  Java_SafeBrowsingApiBridge_isVerifyAppsEnabled(env, callback_id);
}

void SafeBrowsingApiHandlerBridge::StartEnableVerifyApps(
    VerifyAppsResponseCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    std::move(callback).Run(VerifyAppsEnabledResult::FAILED);
    return;
  }

  int64_t callback_id = next_verify_apps_callback_id_++;
  GetPendingVerifyAppsCallbacks().insert({callback_id, std::move(callback)});
  Java_SafeBrowsingApiBridge_enableVerifyApps(env, callback_id);
}

void SafeBrowsingApiHandlerBridge::StartHasPotentiallyHarmfulApps(
    HasHarmfulAppsResponseCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (harmful_apps_result_for_testing_.has_value()) {
    auto result_for_test = harmful_apps_result_for_testing_.value();
    std::move(callback).Run(std::get<0>(result_for_test),
                            std::get<1>(result_for_test),
                            std::get<2>(result_for_test));
    return;
  }

  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
    std::move(callback).Run(HasHarmfulAppsResultStatus::LOCAL_FAILURE, 0, 0);
    return;
  }

  int64_t callback_id = next_harmful_apps_callback_id_++;
  GetPendingHarmfulAppsCallbacks().insert({callback_id, std::move(callback)});
  Java_SafeBrowsingApiBridge_hasPotentiallyHarmfulApps(env, callback_id);
}

void SafeBrowsingApiHandlerBridge::StartGetSafetyNetId(
    GetSafetyNetIdResponseCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (safety_net_id_.has_value()) {
    std::move(callback).Run(*safety_net_id_);
    return;
  }

  JNIEnv* env = AttachCurrentThread();
  if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(
          env, /*requireFirstParty=*/true)) {
    std::move(callback).Run(std::string());
    return;
  }

  // Add a callback to cache the first value we obtain.
  if (pending_get_safety_net_id_callbacks_.empty()) {
    // Unretained is safe because `this` owns the callback subscription.
    pending_get_safety_net_id_callbacks_.push_back(
        GetPendingGetSafetyNetIdCallbacks().Add(
            base::BindOnce(&SafeBrowsingApiHandlerBridge::CacheSafetyNetId,
                           base::Unretained(this))));
  }

  pending_get_safety_net_id_callbacks_.push_back(
      GetPendingGetSafetyNetIdCallbacks().Add(std::move(callback)));
  Java_SafeBrowsingApiBridge_getSafetyNetId(env);
}

void SafeBrowsingApiHandlerBridge::CacheSafetyNetId(const std::string& result) {
  // If the result was empty, there was an unrecoverable error. Cache the result
  // anyway so we don't keep retrying and getting the same error.
  safety_net_id_ = result;
}

void SafeBrowsingApiHandlerBridge::ResetSafetyNetIdForTesting() {
  safety_net_id_.reset();
  pending_get_safety_net_id_callbacks_.clear();
}

void SafeBrowsingApiHandlerBridge::OnSafeBrowsingApiNonRecoverableFailure() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  is_safe_browsing_api_available_ = false;
}

}  // namespace safe_browsing

DEFINE_JNI(SafeBrowsingApiBridge)
