// Copyright 2024 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/digital_credentials/digital_identity_provider_desktop.h"

#include <memory>
#include <variant>

#include "base/containers/span.h"
#include "base/values.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/digital_credentials/digital_identity_low_risk_origins.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/ui/views/accessibility/theme_tracking_non_accessible_image_view.h"
#include "chrome/browser/ui/views/digital_credentials/digital_identity_bluetooth_manual_dialog_controller.h"
#include "chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.h"
#include "chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/qr_code_generator/bitmap_generator.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/cross_device_request_info.h"
#include "content/public/browser/digital_credentials_cross_device.h"
#include "content/public/browser/digital_identity_provider.h"
#include "content/public/browser/web_contents.h"
#include "crypto/random.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/cable/v2_handshake.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/theme_tracking_animated_image_view.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/widget/widget.h"

namespace {

// Smaller than DistanceMetric::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH.
constexpr int kQrCodeSize = 240;
constexpr int kQrCodeMargin = 20;

using DigitalIdentityInterstitialAbortCallback =
    content::DigitalIdentityProvider::DigitalIdentityInterstitialAbortCallback;
using RequestStatusForMetrics =
    content::DigitalIdentityProvider::RequestStatusForMetrics;
using content::digital_credentials::cross_device::Error;
using content::digital_credentials::cross_device::Event;
using content::digital_credentials::cross_device::ProtocolError;
using content::digital_credentials::cross_device::RemoteError;
using content::digital_credentials::cross_device::RequestInfo;
using content::digital_credentials::cross_device::Response;
using content::digital_credentials::cross_device::SystemError;
using content::digital_credentials::cross_device::SystemEvent;
using content::digital_credentials::cross_device::Transaction;

void RunDigitalIdentityCallback(
    std::unique_ptr<DigitalIdentitySafetyInterstitialControllerDesktop>
        controller,
    content::DigitalIdentityProvider::DigitalIdentityInterstitialCallback
        callback,
    content::DigitalIdentityProvider::RequestStatusForMetrics
        status_for_metrics) {
  std::move(callback).Run(status_for_metrics);
}

std::unique_ptr<views::View> MakeQrCodeImageView(const std::string& qr_url) {
  auto qr_code = qr_code_generator::GenerateImage(
      base::as_byte_span(qr_url), qr_code_generator::ModuleStyle::kCircles,
      qr_code_generator::LocatorStyle::kRounded,
      qr_code_generator::CenterImage::kNoCenterImage,
      qr_code_generator::QuietZone::kIncluded);

  // Success is guaranteed, because `qr_url`'s size is bounded and smaller
  // than QR code limits.
  CHECK(qr_code.has_value());
  auto image_view = std::make_unique<views::ImageView>(
      ui::ImageModel::FromImageSkia(qr_code.value()));
  image_view->GetViewAccessibility().SetName(
      l10n_util::GetStringUTF16(IDS_WEB_DIGITAL_CREDENTIALS_QR_CODE_ALT_TEXT));
  image_view->SetImageSize(gfx::Size(kQrCodeSize, kQrCodeSize));
  image_view->SetCornerRadius(
      10);  // Set radius to match that used in the QR-Code Locator.
  image_view->SetBorder(views::CreateEmptyBorder(gfx::Insets(kQrCodeMargin)));
  return image_view;
}

device::cablev2::CredentialRequestType
CrossDeviceRequestTypeToCredentialRequestType(
    RequestInfo::RequestType request_type) {
  switch (request_type) {
    case RequestInfo::RequestType::kGet:
      return device::cablev2::CredentialRequestType::kPresentation;
    case RequestInfo::RequestType::kCreate:
      return device::cablev2::CredentialRequestType::kIssuance;
  }
}

}  // anonymous namespace

DigitalIdentityProviderDesktop::DigitalIdentityProviderDesktop() = default;

DigitalIdentityProviderDesktop::~DigitalIdentityProviderDesktop() = default;

bool DigitalIdentityProviderDesktop::IsLastCommittedOriginLowRisk(
    content::RenderFrameHost& render_frame_host) const {
  return digital_credentials::IsLastCommittedOriginLowRisk(render_frame_host);
}

DigitalIdentityInterstitialAbortCallback
DigitalIdentityProviderDesktop::ShowDigitalIdentityInterstitial(
    content::WebContents& web_contents,
    const url::Origin& origin,
    content::DigitalIdentityInterstitialType interstitial_type,
    DigitalIdentityInterstitialCallback callback) {
  auto controller =
      std::make_unique<DigitalIdentitySafetyInterstitialControllerDesktop>();
  // Callback takes ownership of |controller|.
  return controller->ShowInterstitial(
      web_contents, origin, interstitial_type,
      base::BindOnce(&RunDigitalIdentityCallback, std::move(controller),
                     std::move(callback)));
}

void DigitalIdentityProviderDesktop::Get(content::WebContents* web_contents,
                                         const url::Origin& rp_origin,
                                         base::ValueView request,
                                         DigitalIdentityCallback callback) {
  Transact(web_contents, RequestInfo::RequestType::kGet, rp_origin, request,
           std::move(callback));
}

void DigitalIdentityProviderDesktop::Create(content::WebContents* web_contents,
                                            const url::Origin& rp_origin,
                                            base::ValueView request,
                                            DigitalIdentityCallback callback) {
  Transact(web_contents, RequestInfo::RequestType::kCreate, rp_origin, request,
           std::move(callback));
}

void DigitalIdentityProviderDesktop::Transact(
    content::WebContents* web_contents,
    RequestInfo::RequestType request_type,
    const url::Origin& rp_origin,
    base::ValueView request,
    DigitalIdentityCallback callback) {
  web_contents_ = web_contents->GetWeakPtr();
  rp_origin_ = rp_origin;
  callback_ = std::move(callback);

  RequestInfo request_info{request_type, rp_origin, request.ToValue()};

  std::array<uint8_t, device::cablev2::kQRKeySize> qr_generator_key;
  crypto::RandBytes(qr_generator_key);

  std::string qr_url = device::cablev2::qr::Encode(
      qr_generator_key,
      CrossDeviceRequestTypeToCredentialRequestType(request_type));

  transaction_ = Transaction::New(
      std::move(request_info), qr_generator_key, base::BindRepeating([]() {
        return SystemNetworkContextManager::GetInstance()->GetContext();
      }),
      base::BindRepeating(&DigitalIdentityProviderDesktop::OnEvent,
                          weak_ptr_factory_.GetWeakPtr(), std::move(qr_url),
                          request_type),
      base::BindOnce(&DigitalIdentityProviderDesktop::OnFinished,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DigitalIdentityProviderDesktop::OnEvent(
    const std::string& qr_url,
    RequestInfo::RequestType request_type,
    Event event) {
  std::visit(absl::Overload{
                 [this, qr_url, request_type](SystemEvent event) {
                   switch (event) {
                     case SystemEvent::kBluetoothNotPowered:
                       ShowBluetoothManualTurnOnDialog();
                       break;
                     case SystemEvent::kNeedPermission:
                       // The user is being asked for Bluetooth permission by
                       // the system. Nothing for Chrome UI to do.
                       break;
                     case SystemEvent::kReady:
                       bluetooth_manual_dialog_controller_.reset();
                       ShowQrCodeDialog(qr_url, request_type);
                       break;
                   }
                 },
                 [this](device::cablev2::Event event) { OnCableEvent(event); },
             },
             event);
}

void DigitalIdentityProviderDesktop::OnCableEvent(
    device::cablev2::Event event) {
  switch (event) {
    case device::cablev2::Event::kPhoneConnected:
    case device::cablev2::Event::kBLEAdvertReceived:
      ShowConnectingToPhoneDialog();
      if (!cable_connecting_dialog_timer_.IsRunning()) {
        cable_connecting_dialog_timer_.Start(
            FROM_HERE, base::Milliseconds(2500),
            base::BindOnce(
                &DigitalIdentityProviderDesktop::OnCableConnectingTimerComplete,
                weak_ptr_factory_.GetWeakPtr()));
      }
      break;
    case device::cablev2::Event::kReady:
      // If we are ready before the timer fires, don't show the next dialog
      // directly to make sure the "connecting to phone" dialog is visible for
      // long enough time to avoid flashing the UI. Otherwise, show the next
      // dialog directly.
      if (cable_connecting_dialog_timer_.IsRunning()) {
        cable_connecting_ready_to_advance_ = true;
      } else {
        ShowContinueStepsOnThePhoneDialog();
      }
      break;
  }
}

void DigitalIdentityProviderDesktop::OnFinished(
    base::expected<Response, Error> result) {
  if (result.has_value()) {
    std::move(callback_).Run(std::move(result.value().value()));
    return;
  }

  RequestStatusForMetrics status;
  std::visit(
      absl::Overload{[&status](SystemError error) {
                       status = RequestStatusForMetrics::kErrorOther;
                     },
                     [&status](ProtocolError error) {
                       status = RequestStatusForMetrics::kErrorOther;
                     },
                     [&status](RemoteError error) {
                       switch (error) {
                         case RemoteError::kNoCredential:
                           status = RequestStatusForMetrics::kErrorNoCredential;
                           break;
                         case RemoteError::kUserCanceled:
                           status = RequestStatusForMetrics::kErrorUserDeclined;
                           break;
                         case RemoteError::kDeviceAborted:
                           status = RequestStatusForMetrics::kErrorAborted;
                           break;
                         case RemoteError::kOther:
                           status = RequestStatusForMetrics::kErrorOther;
                           break;
                       }
                     }},
      result.error());
  EndRequestWithError(status);
  // NOTE: `EndRequestWithError` may delete `this`, so it must be the last
  // thing called in this method.
}

DigitalIdentityMultiStepDialog*
DigitalIdentityProviderDesktop::EnsureDialogCreated() {
  if (!dialog_) {
    dialog_ = std::make_unique<DigitalIdentityMultiStepDialog>(web_contents_);
  }
  return dialog_.get();
}

void DigitalIdentityProviderDesktop::ShowQrCodeDialog(
    const std::string& qr_url,
    RequestInfo::RequestType request_type) {
  std::u16string dialog_title =
      l10n_util::GetStringUTF16(IDS_WEB_DIGITAL_CREDENTIALS_QR_TITLE);

  int dialog_body_id = 0;
  switch (request_type) {
    case RequestInfo::RequestType::kGet:
      dialog_body_id = IDS_WEB_DIGITAL_CREDENTIALS_PRESENTATION_QR_BODY;
      break;
    case RequestInfo::RequestType::kCreate:
      dialog_body_id = IDS_WEB_DIGITAL_CREDENTIALS_ISSUANCE_QR_BODY;
      break;
  }
  const views::LayoutProvider* layout_provider = views::LayoutProvider::Get();
  // The dialog content width is used as an approximation of the available
  // width for the origin.
  const int dialog_width = layout_provider->GetDistanceMetric(
      views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH);
  const gfx::Insets dialog_insets =
      layout_provider->GetInsetsMetric(views::INSETS_DIALOG);
  const float content_width = dialog_width - dialog_insets.width();
  const gfx::FontList& font_list = views::TypographyProvider::Get().GetFont(
      views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY);
  std::u16string formatted_origin =
      url_formatter::ElideUrl(rp_origin_.GetURL(), font_list, content_width);

  std::u16string dialog_body =
      l10n_util::GetStringFUTF16(dialog_body_id, formatted_origin);
  EnsureDialogCreated()->TryShow(
      /*accept_button=*/std::nullopt, base::OnceClosure(),
      /*cancel_button=*/
      ui::DialogModel::Button::Params()
          .SetLabel(l10n_util::GetStringUTF16(
              IDS_WEB_DIGITAL_CREDENTIALS_FLOW_CANCEL_BUTTON_TEXT))
          .SetStyle(ui::ButtonStyle::kDefault),
      base::BindOnce(&DigitalIdentityProviderDesktop::OnCanceled,
                     weak_ptr_factory_.GetWeakPtr()),
      dialog_title, dialog_body, MakeQrCodeImageView(qr_url),
      /*show_progress_bar=*/false);
}

void DigitalIdentityProviderDesktop::ShowBluetoothManualTurnOnDialog() {
  bluetooth_manual_dialog_controller_ =
      std::make_unique<DigitalIdentityBluetoothManualDialogController>(
          EnsureDialogCreated());
  bluetooth_manual_dialog_controller_->Show(
      base::BindOnce(
          &DigitalIdentityProviderDesktop::OnUserRequestedBluetoothPowerOn,
          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&DigitalIdentityProviderDesktop::OnCanceled,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DigitalIdentityProviderDesktop::OnUserRequestedBluetoothPowerOn() {
  transaction_->PowerBluetoothAdapter();
}

void DigitalIdentityProviderDesktop::ShowConnectingToPhoneDialog() {
  // Ensure the dialog is created to have access to GetBackgroundColor().
  EnsureDialogCreated();
  std::u16string title_text = l10n_util::GetStringUTF16(
      IDS_WEB_DIGITAL_CREDENTIALS_CABLEV2_CONNECTING_TITLE);
  auto illustration = std::make_unique<views::ThemeTrackingAnimatedImageView>(
      IDR_WEBAUTHN_HYBRID_CONNECTING_LIGHT, IDR_WEBAUTHN_HYBRID_CONNECTING_DARK,
      base::BindRepeating(&DigitalIdentityMultiStepDialog::GetBackgroundColor,
                          base::Unretained(dialog_.get())));

  EnsureDialogCreated()->TryShow(
      /*accept_button=*/std::nullopt, base::OnceClosure(),
      /*cancel_button=*/
      ui::DialogModel::Button::Params()
          .SetLabel(l10n_util::GetStringUTF16(
              IDS_WEB_DIGITAL_CREDENTIALS_FLOW_CANCEL_BUTTON_TEXT))
          .SetStyle(ui::ButtonStyle::kDefault),
      base::BindOnce(&DigitalIdentityProviderDesktop::OnCanceled,
                     weak_ptr_factory_.GetWeakPtr()),
      /*dialog_title=*/u"", /*dialog_body=*/u"",
      DigitalIdentityMultiStepDialog::CreateHeaderView(
          std::move(title_text), /*body_text=*/u"", std::move(illustration)),
      /*show_progress_bar=*/true);
}

void DigitalIdentityProviderDesktop::ShowContinueStepsOnThePhoneDialog() {
  // Ensure the dialog is created to have access to GetBackgroundColor().
  EnsureDialogCreated();
  std::u16string title_text = l10n_util::GetStringUTF16(
      IDS_WEB_DIGITAL_CREDENTIALS_CABLEV2_CONNECTED_TITLE);
  auto illustration = std::make_unique<ThemeTrackingNonAccessibleImageView>(
      ui::ImageModel::FromVectorIcon(kPasskeyPhoneCustomIcon),
      ui::ImageModel::FromVectorIcon(kPasskeyPhoneDarkCustomIcon),
      base::BindRepeating(&DigitalIdentityMultiStepDialog::GetBackgroundColor,
                          base::Unretained(dialog_.get())));

  EnsureDialogCreated()->TryShow(
      /*accept_button=*/std::nullopt, base::OnceClosure(),
      /*cancel_button=*/
      ui::DialogModel::Button::Params()
          .SetLabel(l10n_util::GetStringUTF16(
              IDS_WEB_DIGITAL_CREDENTIALS_FLOW_CANCEL_BUTTON_TEXT))
          .SetStyle(ui::ButtonStyle::kDefault),
      base::BindOnce(&DigitalIdentityProviderDesktop::OnCanceled,
                     weak_ptr_factory_.GetWeakPtr()),
      /*dialog_title=*/u"", /*dialog_body=*/u"",
      DigitalIdentityMultiStepDialog::CreateHeaderView(
          std::move(title_text), /*body_text=*/u"", std::move(illustration)),
      /*show_progress_bar=*/true);
}

void DigitalIdentityProviderDesktop::OnCableConnectingTimerComplete() {
  if (cable_connecting_ready_to_advance_) {
    ShowContinueStepsOnThePhoneDialog();
  }
}

void DigitalIdentityProviderDesktop::OnCanceled() {
  EndRequestWithError(RequestStatusForMetrics::kErrorUserDeclined);
}

void DigitalIdentityProviderDesktop::EndRequestWithError(
    RequestStatusForMetrics status) {
  if (callback_.is_null()) {
    return;
  }

  // `dialog_.reset()` can synchronously close the UI which (via activation
  // observers) may destroy the hosting WebContents, resulting in the
  // synchronous destruction of the frame-bound Mojo DocumentService
  // `DigitalIdentityRequestImpl` and therefore `this`.
  //
  // To avoid a Use-After-Free, move the callback to a local variable on the
  // stack before resetting the dialog or the manual bluetooth controller (both
  // of which could trigger synchronous teardown via UI events).
  auto local_callback = std::move(callback_);

  bluetooth_manual_dialog_controller_.reset();
  dialog_.reset();
  // `this` may be deleted at this point.

  std::move(local_callback).Run(base::unexpected(status));
}
