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

#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate.h"

#include <optional>

#include "base/base64.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/password_manager/core/browser/password_ui_utils.h"
#include "content/public/browser/web_contents.h"
#include "device/fido/public/fido_types.h"
#include "ui/base/l10n/l10n_util.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/webauthn/authenticator_request_dialog_controller.h"
#include "chrome/browser/webauthn/authenticator_request_scheduler.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/webauthn/android/webauthn_request_delegate_android.h"
#endif

namespace {
using device::AuthenticatorType;
#if !BUILDFLAG(IS_ANDROID)
// `AuthenticatorRequestDialogModel` observed from `authenticator_observation_`
// may notify this class too soon, causing a flicker. Delay calling
// `passkey_selected_callback_` at least 300ms to avoid the flicker.
// TODO(crbug.com/332619045): Move this to a UI layer.
constexpr base::TimeDelta kFlickerDuration = base::Milliseconds(300);

bool IsGpmPasskeyAuthenticatorType(AuthenticatorType type) {
  return type == AuthenticatorType::kEnclave;
}
#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace

using password_manager::PasskeyCredential;
using OnPasskeySelectedCallback =
    password_manager::WebAuthnCredentialsDelegate::OnPasskeySelectedCallback;

ChromeWebAuthnCredentialsDelegate::ChromeWebAuthnCredentialsDelegate(
    content::RenderFrameHost* frame_host)
    : frame_host_(frame_host),
      web_contents_(content::WebContents::FromRenderFrameHost(frame_host)) {}

ChromeWebAuthnCredentialsDelegate::~ChromeWebAuthnCredentialsDelegate() =
    default;

void ChromeWebAuthnCredentialsDelegate::LaunchSecurityKeyOrHybridFlow() {
#if !BUILDFLAG(IS_ANDROID)
  ChromeAuthenticatorRequestDelegate* authenticator_delegate =
      AuthenticatorRequestScheduler::GetRequestDelegate(web_contents_);
  if (!authenticator_delegate) {
    return;
  }
  authenticator_delegate->dialog_controller()
      ->TransitionToModalWebAuthnRequest();
#else
  if (WebAuthnRequestDelegateAndroid* delegate =
          WebAuthnRequestDelegateAndroid::GetRequestDelegate(frame_host_)) {
    delegate->OnHybridSignInSelected();
  }
#endif  // !BUILDFLAG(IS_ANDROID)
}

void ChromeWebAuthnCredentialsDelegate::SelectPasskey(
    const std::string& backend_id,
    OnPasskeySelectedCallback callback) {
  // `backend_id` is the base64-encoded credential ID. See `PasskeyCredential`
  // for where these are encoded.
  std::optional<std::vector<uint8_t>> selected_credential_id =
      base::Base64Decode(backend_id);
  DCHECK(selected_credential_id);

#if BUILDFLAG(IS_ANDROID)
  std::move(callback).Run();
  auto* request_delegate =
      WebAuthnRequestDelegateAndroid::GetRequestDelegate(frame_host_);
  if (!request_delegate) {
    return;
  }
  request_delegate->OnWebAuthnAccountSelected(*selected_credential_id);
#else
  ChromeAuthenticatorRequestDelegate* authenticator_delegate =
      AuthenticatorRequestScheduler::GetRequestDelegate(web_contents_);
  if (!authenticator_delegate) {
    std::move(callback).Run();
    return;
  }
  if (passkey_selected_callback_) {
    // The user tapped on another passkey while the enclave was loading. Ignore
    // the tap.
    return;
  }
  passkey_selected_callback_ = std::move(callback);
  authenticator_observation_.Observe(authenticator_delegate->dialog_model());
  AuthenticatorType credential_source =
      authenticator_delegate->dialog_controller()->OnAccountPreselected(
          *selected_credential_id);
  // If the credential is not from a GPM authenticator, we do not need to
  // observe the model anymore.
  if (!IsGpmPasskeyAuthenticatorType(credential_source)) {
    authenticator_observation_.Reset();
    std::move(passkey_selected_callback_).Run();
  }
#endif  // BUILDFLAG(IS_ANDROID)
}

std::optional<std::string> ChromeWebAuthnCredentialsDelegate::GetCableQrString()
    const {
#if !BUILDFLAG(IS_ANDROID)
  ChromeAuthenticatorRequestDelegate* authenticator_delegate =
      AuthenticatorRequestScheduler::GetRequestDelegate(web_contents_);
  if (!authenticator_delegate || !authenticator_delegate->dialog_model()) {
    return std::nullopt;
  }
  if (!authenticator_delegate->dialog_model()->ble_adapter_is_powered) {
    return std::nullopt;
  }
  return authenticator_delegate->dialog_model()->cable_qr_string;
#else
  return std::nullopt;
#endif
}

base::expected<const std::vector<PasskeyCredential>*,
               ChromeWebAuthnCredentialsDelegate::PasskeysUnavailableReason>
ChromeWebAuthnCredentialsDelegate::GetPasskeys() const {
  if (!passkeys_) {
    return last_request_was_aborted_
               ? base::unexpected(
                     ChromeWebAuthnCredentialsDelegate::
                         PasskeysUnavailableReason::kRequestAborted)
               : base::unexpected(ChromeWebAuthnCredentialsDelegate::
                                      PasskeysUnavailableReason::kNotReceived);
  }

  return base::ok(&passkeys_.value());
}

void ChromeWebAuthnCredentialsDelegate::NotifyForPasskeysDisplay() {
  passkey_display_has_happened_ = true;
}

base::WeakPtr<password_manager::WebAuthnCredentialsDelegate>
ChromeWebAuthnCredentialsDelegate::AsWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

bool ChromeWebAuthnCredentialsDelegate::HasPendingPasskeySelection() {
#if BUILDFLAG(IS_ANDROID)
  return false;
#else
  return !passkey_selected_callback_.is_null();
#endif  // BUILDFLAG(IS_ANDROID)
}

#if !BUILDFLAG(IS_ANDROID)
void ChromeWebAuthnCredentialsDelegate::OnStepTransition() {
  AuthenticatorRequestDialogModel* model =
      authenticator_observation_.GetSource();
  CHECK(model) << "The model just stepped but not registered as source.";
  if (!passkey_selected_callback_ || !model->preselected_cred.has_value() ||
      !IsGpmPasskeyAuthenticatorType(model->preselected_cred.value().source)) {
    return;
  }
  // Do not dismiss the autofill popup when the AuthenticatorRequestDialogModel
  // says that UI is disabled.
  if (!model->ui_disabled_ &&
      model->step() != AuthenticatorRequestDialogModel::Step::kNotStarted) {
    authenticator_observation_.Reset();
    flickering_timer_.Start(FROM_HERE, kFlickerDuration,
                            std::move(passkey_selected_callback_));
  }
}
#endif  // !BUILDFLAG(IS_ANDROID)

bool ChromeWebAuthnCredentialsDelegate::IsSecurityKeyOrHybridFlowAvailable()
    const {
  return security_key_or_hybrid_flow_available_.value();
}

void ChromeWebAuthnCredentialsDelegate::RequestNotificationWhenPasskeysReady(
    base::OnceClosure callback) {
  if (!passkey_retrieval_timer_started_) {
    passkey_retrieval_timer_ = std::make_unique<base::ElapsedTimer>();
    passkey_retrieval_timer_started_ = true;
  }
  if (passkeys_.has_value()) {
    RecordPasskeyRetrievalDelay();
    // Entries were already populated from the WebAuthn request.
    std::move(callback).Run();
    return;
  }

  passkeys_available_callbacks_.push_back(std::move(callback));
}

void ChromeWebAuthnCredentialsDelegate::OnCredentialsReceived(
    std::vector<PasskeyCredential> credentials,
    SecurityKeyOrHybridFlowAvailable security_key_or_hybrid_flow_available) {
  last_request_was_aborted_ = false;
  if (!credentials.empty() && !passkeys_after_fill_recorded_) {
    passkeys_after_fill_recorded_ = true;
    base::UmaHistogramBoolean(
        "PasswordManager.PasskeysArrivedAfterAutofillDisplay",
        passkey_display_has_happened_);
  }

  passkeys_ = std::move(credentials);
  security_key_or_hybrid_flow_available_ =
      security_key_or_hybrid_flow_available;
  RecordPasskeyRetrievalDelay();
  NotifyClientsOfPasskeyAvailability();
}

void ChromeWebAuthnCredentialsDelegate::NotifyWebAuthnRequestAborted() {
  passkeys_ = std::nullopt;
  last_request_was_aborted_ = true;
  NotifyClientsOfPasskeyAvailability();
#if !BUILDFLAG(IS_ANDROID)
  // Also dismiss the autofill popup if it is being displayed and a webauthn
  // request is loading.
  if (passkey_selected_callback_) {
    flickering_timer_.Start(FROM_HERE, kFlickerDuration,
                            std::move(passkey_selected_callback_));
  }
  authenticator_observation_.Reset();
#endif  // BUILDFLAG(IS_ANDROID)
}

void ChromeWebAuthnCredentialsDelegate::RecordPasskeyRetrievalDelay() {
  if (passkey_retrieval_timer_) {
    base::UmaHistogramTimes("PasswordManager.PasskeyRetrievalWaitDuration",
                            passkey_retrieval_timer_->Elapsed());
    passkey_retrieval_timer_.reset();
  }
}

void ChromeWebAuthnCredentialsDelegate::NotifyClientsOfPasskeyAvailability() {
  std::vector<base::OnceClosure> callbacks;
  callbacks.swap(passkeys_available_callbacks_);

  for (auto& callback : callbacks) {
    std::move(callback).Run();
  }
}
