// Copyright 2018 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/webauthn/authenticator_request_dialog_model.h"

#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "base/containers/fixed_flat_map.h"
#include "base/functional/callback.h"
#include "base/observer_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/webauthn/authenticator_request_dialog_view_controller.h"
#include "chrome/browser/ui/webauthn/authenticator_request_window.h"
#include "chrome/browser/webauthn/authenticator_transport.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/device_event_log/device_event_log.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/public/fido_types.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h"

namespace {

// StepUiType enumerates the different types of UI that can be displayed.
enum class StepUIType {
  NONE,
  // A Chromium captive dialog.
  DIALOG,
  // A top-level window.
  WINDOW,
};

StepUIType step_ui_type(AuthenticatorRequestDialogModel::Step step) {
  switch (step) {
    case AuthenticatorRequestDialogModel::Step::kClosed:
    case AuthenticatorRequestDialogModel::Step::kNotStarted:
    case AuthenticatorRequestDialogModel::Step::kPasskeyAutofill:
    case AuthenticatorRequestDialogModel::Step::kPasskeyUpgrade:
    case AuthenticatorRequestDialogModel::Step::kPasswordOsAuth:
    case AuthenticatorRequestDialogModel::Step::kPlatformAuthenticator:
      return StepUIType::NONE;

    case AuthenticatorRequestDialogModel::Step::kGPMRecoverSecurityDomain:
    case AuthenticatorRequestDialogModel::Step::kGPMReauthForPinReset:
      return StepUIType::WINDOW;

    default:
      return StepUIType::DIALOG;
  }
}

std::optional<content::GlobalRenderFrameHostId> FrameHostIdFromMaybeNull(
    content::RenderFrameHost* render_frame_host) {
  if (render_frame_host == nullptr) {
    return std::nullopt;
  }
  return render_frame_host->GetGlobalId();
}

content::WebContents* GetWebContentsFromFrameHostId(
    std::optional<content::GlobalRenderFrameHostId> frame_host_id) {
  if (!frame_host_id) {
    return nullptr;
  }
  return content::WebContents::FromRenderFrameHost(
      content::RenderFrameHost::FromID(*frame_host_id));
}

}  // namespace

#define AUTHENTICATOR_REQUEST_EVENT_0(name) \
  void AuthenticatorRequestDialogModel::Observer::name() {}
#define AUTHENTICATOR_REQUEST_EVENT_1(name, arg1type) \
  void AuthenticatorRequestDialogModel::Observer::name(arg1type) {}
AUTHENTICATOR_EVENTS
#undef AUTHENTICATOR_REQUEST_EVENT_0
#undef AUTHENTICATOR_REQUEST_EVENT_1

// static
std::u16string AuthenticatorRequestDialogModel::GetMechanismDescription(
    const device::DiscoverableCredentialMetadata& cred,
    UIPresentation ui_presentation) {
  bool immediate_mode = UIPresentation::kModalImmediate == ui_presentation;
  if (cred.provider_name) {
    return immediate_mode ? l10n_util::GetStringFUTF16(
                                IDS_PASSWORD_MANAGER_PASSKEY_FROM_PROVIDER,
                                base::UTF8ToUTF16(*cred.provider_name))
                          : base::UTF8ToUTF16(*cred.provider_name);
  }
  int message;
  switch (cred.source) {
    case device::AuthenticatorType::kWinNative:
      message = immediate_mode ? IDS_PASSWORD_MANAGER_PASSKEY_FROM_WINDOWS_HELLO
                               : IDS_WEBAUTHN_SOURCE_WINDOWS_HELLO;
      break;
    case device::AuthenticatorType::kTouchID:
      message = immediate_mode
                    ? IDS_PASSWORD_MANAGER_PASSKEY_FROM_CHROME_PROFILE
                    : IDS_WEBAUTHN_SOURCE_CHROME_PROFILE;
      break;
    case device::AuthenticatorType::kICloudKeychain:
      message = immediate_mode
                    ? IDS_PASSWORD_MANAGER_PASSKEY_FROM_ICLOUD_KEYCHAIN
                    : IDS_WEBAUTHN_SOURCE_ICLOUD_KEYCHAIN;
      break;
    case device::AuthenticatorType::kEnclave:
      message = immediate_mode
                    ? IDS_PASSWORD_MANAGER_PASSKEY_FROM_GOOGLE_PASSWORD_MANAGER
                    : IDS_WEBAUTHN_SOURCE_GOOGLE_PASSWORD_MANAGER;
      break;
    case device::AuthenticatorType::kOther:
      // "Other" is USB security keys and the virtual authenticator.
      CHECK(!immediate_mode);
      message = IDS_WEBAUTHN_SOURCE_USB_SECURITY_KEY;
      break;
    default:
      message = IDS_PASSWORD_MANAGER_USE_GENERIC_DEVICE;
  }
  return l10n_util::GetStringUTF16(message);
}

AuthenticatorRequestDialogModel::AuthenticatorRequestDialogModel(
    content::RenderFrameHost* render_frame_host)
    : frame_host_id(FrameHostIdFromMaybeNull(render_frame_host)) {}

AuthenticatorRequestDialogModel::~AuthenticatorRequestDialogModel() {
  for (auto& observer : observers) {
    observer.OnModelDestroyed(this);
  }
}

void AuthenticatorRequestDialogModel::AddObserver(
    AuthenticatorRequestDialogModel::Observer* observer) {
  observers.AddObserver(observer);
}

void AuthenticatorRequestDialogModel::RemoveObserver(
    AuthenticatorRequestDialogModel::Observer* observer) {
  observers.RemoveObserver(observer);
}

void AuthenticatorRequestDialogModel::SetStep(Step step) {
  FIDO_LOG(EVENT) << "UI step: " << step;

  const StepUIType previous_ui_type = step_ui_type(step_);
  step_ = step;
  ui_disabled_ = false;

  const StepUIType ui_type = step_ui_type(step_);
  auto* web_contents = GetWebContentsFromFrameHostId(frame_host_id);
  if (ui_type != StepUIType::DIALOG) {
    view_controller_.reset();
    if (ui_type == StepUIType::WINDOW &&
        previous_ui_type != StepUIType::WINDOW && web_contents) {
      ShowAuthenticatorRequestWindow(web_contents, this);
    }
  } else if (previous_ui_type != StepUIType::DIALOG && web_contents) {
    view_controller_ =
        AuthenticatorRequestDialogViewController::Create(web_contents, this);
  }

  for (auto& observer : observers) {
    observer.OnStepTransition();
  }
}

void AuthenticatorRequestDialogModel::DisableUiOrShowLoadingDialog() {
  // If the current step is showing a dialog, disable it. Else, show the GPM
  // Connecting dialog. The native Touch ID control cannot be effectively
  // disabled so that sheet is an exception.
  if (step() != Step::kPasskeyAutofill &&
      (should_dialog_be_closed() || step() == Step::kGPMTouchID)) {
    SetStep(Step::kGPMConnecting);
  } else {
    ui_disabled_ = true;
    OnSheetModelChanged();
  }
}

bool AuthenticatorRequestDialogModel::should_dialog_be_closed() const {
  return step_ui_type(step_) != StepUIType::DIALOG;
}

std::optional<AccountInfo>
AuthenticatorRequestDialogModel::GetGpmAccountInfo() {
  Profile* profile = GetProfile();
  if (!profile) {
    return std::nullopt;
  }
  signin::IdentityManager* identity_manager =
      IdentityManagerFactory::GetForProfile(profile->GetOriginalProfile());
  if (!identity_manager) {
    return std::nullopt;
  }
  CoreAccountInfo core_account_info =
      identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  CHECK(!core_account_info.IsEmpty());
  return identity_manager->FindExtendedAccountInfo(core_account_info);
}

std::string AuthenticatorRequestDialogModel::GetGpmAccountEmail() {
  std::optional<AccountInfo> account_info = GetGpmAccountInfo();
  if (!account_info) {
    return "";
  }
  return account_info->email;
}

Profile* AuthenticatorRequestDialogModel::GetProfile() {
  content::RenderFrameHost* rfh =
      content::RenderFrameHost::FromID(*frame_host_id);
  if (!rfh) {
    return nullptr;
  }
  return Profile::FromBrowserContext(rfh->GetBrowserContext());
}

#define AUTHENTICATOR_REQUEST_EVENT_0(name)        \
  void AuthenticatorRequestDialogModel::name() {   \
    const int start_generation = this->generation; \
    for (auto& observer : observers) {             \
      if (start_generation != this->generation) {  \
        break;                                     \
      }                                            \
      observer.name();                             \
    }                                              \
  }
#define AUTHENTICATOR_REQUEST_EVENT_1(name, arg1type)         \
  void AuthenticatorRequestDialogModel::name(arg1type arg1) { \
    const int start_generation = this->generation;            \
    for (auto& observer : observers) {                        \
      if (start_generation != this->generation) {             \
        break;                                                \
      }                                                       \
      observer.name(arg1);                                    \
    }                                                         \
  }
AUTHENTICATOR_EVENTS
#undef AUTHENTICATOR_REQUEST_EVENT_0
#undef AUTHENTICATOR_REQUEST_EVENT_1

std::ostream& operator<<(std::ostream& os,
                         const AuthenticatorRequestDialogModel::Step& step) {
  using Step = AuthenticatorRequestDialogModel::Step;
  constexpr auto kStepNames = base::MakeFixedFlatMap<Step, std::string_view>({
      {Step::kNotStarted, "kNotStarted"},
      {Step::kPasskeyAutofill, "kPasskeyAutofill"},
      {Step::kPasskeyUpgrade, "kPasskeyUpgrade"},
      {Step::kMechanismSelection, "kMechanismSelection"},
      {Step::kErrorNoAvailableTransports, "kErrorNoAvailableTransports"},
      {Step::kErrorNoPasskeys, "kErrorNoPasskeys"},
      {Step::kErrorGpmDisabled, "kErrorGpmDisabled"},
      {Step::kErrorInternalUnrecognized, "kErrorInternalUnrecognized"},
      {Step::kErrorWindowsHelloNotEnabled, "kErrorWindowsHelloNotEnabled"},
      {Step::kTimedOut, "kTimedOut"},
      {Step::kKeyNotRegistered, "kKeyNotRegistered"},
      {Step::kKeyAlreadyRegistered, "kKeyAlreadyRegistered"},
      {Step::kMissingCapability, "kMissingCapability"},
      {Step::kStorageFull, "kStorageFull"},
      {Step::kClosed, "kClosed"},
      {Step::kUsbInsertAndActivate, "kUsbInsertAndActivate"},
      {Step::kBlePowerOnAutomatic, "kBlePowerOnAutomatic"},
      {Step::kBlePowerOnManual, "kBlePowerOnManual"},
      {Step::kBlePermissionMac, "kBlePermissionMac"},
      {Step::kOffTheRecordInterstitial, "kOffTheRecordInterstitial"},
      {Step::kCableV2QRCode, "kCableV2QRCode"},
      {Step::kCableV2Connecting, "kCableV2Connecting"},
      {Step::kCableV2Connected, "kCableV2Connected"},
      {Step::kCableV2Error, "kCableV2Error"},
      {Step::kClientPinChange, "kClientPinChange"},
      {Step::kClientPinEntry, "kClientPinEntry"},
      {Step::kClientPinSetup, "kClientPinSetup"},
      {Step::kClientPinTapAgain, "kClientPinTapAgain"},
      {Step::kClientPinErrorSoftBlock, "kClientPinErrorSoftBlock"},
      {Step::kClientPinErrorHardBlock, "kClientPinErrorHardBlock"},
      {Step::kClientPinErrorAuthenticatorRemoved,
       "kClientPinErrorAuthenticatorRemoved"},
      {Step::kInlineBioEnrollment, "kInlineBioEnrollment"},
      {Step::kRetryInternalUserVerification, "kRetryInternalUserVerification"},
      {Step::kResidentCredentialConfirmation,
       "kResidentCredentialConfirmation"},
      {Step::kSelectAccount, "kSelectAccount"},
      {Step::kPreSelectAccount, "kPreSelectAccount"},
      {Step::kSelectPriorityMechanism, "kSelectPriorityMechanism"},
      {Step::kGPMChangePin, "kGPMChangePin"},
      {Step::kGPMCreatePin, "kGPMCreatePin"},
      {Step::kGPMEnterPin, "kGPMEnterPin"},
      {Step::kGPMChangeArbitraryPin, "kGPMChangeArbitraryPin"},
      {Step::kGPMCreateArbitraryPin, "kGPMCreateArbitraryPin"},
      {Step::kGPMEnterArbitraryPin, "kGPMEnterArbitraryPin"},
      {Step::kGPMTouchID, "kGPMTouchID"},
      {Step::kGPMCreatePasskey, "kGPMCreatePasskey"},
      {Step::kChromeProfileCreatePasskey, "kChromeProfileCreatePasskey"},
      {Step::kGPMError, "kGPMError"},
      {Step::kGPMConnecting, "kGPMConnecting"},
      {Step::kGPMRecoverSecurityDomain, "kGPMRecoverSecurityDomain"},
      {Step::kGPMTrustThisComputerAssertion, "kGPMTrustThisComputerAssertion"},
      {Step::kGPMTrustThisComputerCreation, "kGPMTrustThisComputerCreation"},
      {Step::kGPMReauthForPinReset, "kGPMReauthForPinReset"},
      {Step::kGPMLockedPin, "kGPMLockedPin"},
      {Step::kPasswordOsAuth, "kPasswordAuth"},
      {Step::kPlatformAuthenticator, "kPlatformAuthenticator"},
  });
  static_assert(Step::kMaxValue == Step::kPlatformAuthenticator &&
                    kStepNames.size() - 1 == static_cast<int>(Step::kMaxValue),
                "implement operator<< overload when adding new Step values");
  return os << kStepNames.at(step);
}

AuthenticatorRequestDialogModel::Mechanism::Mechanism(
    AuthenticatorRequestDialogModel::Mechanism::Type in_type,
    std::u16string in_name,
    const gfx::VectorIcon& in_icon,
    base::RepeatingClosure in_callback,
    std::u16string in_display_name)
    : type(std::move(in_type)),
      name(std::move(in_name)),
      display_name(std::move(in_display_name)),
      icon(in_icon),
      callback(std::move(in_callback)) {}
AuthenticatorRequestDialogModel::Mechanism::~Mechanism() = default;
AuthenticatorRequestDialogModel::Mechanism::Mechanism(Mechanism&&) = default;

AuthenticatorRequestDialogModel::Mechanism::CredentialInfo::CredentialInfo(
    device::AuthenticatorType source_in,
    std::vector<uint8_t> user_id_in,
    std::optional<base::Time> last_used_time_in)
    : source(source_in),
      user_id(std::move(user_id_in)),
      last_used_time(last_used_time_in) {}
AuthenticatorRequestDialogModel::Mechanism::CredentialInfo::CredentialInfo(
    const CredentialInfo&) = default;
AuthenticatorRequestDialogModel::Mechanism::CredentialInfo::~CredentialInfo() =
    default;
bool AuthenticatorRequestDialogModel::Mechanism::CredentialInfo::operator==(
    const CredentialInfo&) const = default;

AuthenticatorRequestDialogModel::Mechanism::PasswordInfo::PasswordInfo(
    std::optional<base::Time> last_used_time_in,
    std::optional<std::u16string> origin_in)
    : last_used_time(std::move(last_used_time_in)),
      origin(std::move(origin_in)) {}
AuthenticatorRequestDialogModel::Mechanism::PasswordInfo::PasswordInfo(
    const PasswordInfo&) = default;
AuthenticatorRequestDialogModel::Mechanism::PasswordInfo::~PasswordInfo() =
    default;
bool AuthenticatorRequestDialogModel::Mechanism::PasswordInfo::operator==(
    const PasswordInfo&) const = default;
