// Copyright 2022 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/local_credential_management_win.h"

#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/task/sequenced_task_runner.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_observer.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/user_prefs/user_prefs.h"
#include "device/fido/win/authenticator.h"
#include "device/fido/win/webauthn_api.h"

namespace {

bool ContainsUserCreatedCredential(
    const std::vector<device::DiscoverableCredentialMetadata>& credentials) {
  return std::ranges::any_of(
      credentials, [](const device::DiscoverableCredentialMetadata& cred) {
        return !cred.system_created;
      });
}

constexpr char kHasPlatformCredentialsPref[] =
    "webauthn.has_platform_credentials";

// CredentialPresenceCacher caches, in a `Profile` whether local credentials
// were found or not. This is done because we expect that enumerating platform
// credentials on Windows will get slower as the number of credentials
// increases, and we need to know whether there are any credentials in order to
// show the link (or not) on the passwords WebUI page.
//
// Thus, if credentials have been observed previously then that fact is cached
// and the link will appear on the passwords page without enumerating them
// again. Otherwise an enumeration will be attempted, which should be fast in
// the common case that there aren't any credentials.
//
// Since the platform authenticator is system-global, a `Profile` isn't quite
// the right sort of object to cache this information in. However, storing an
// installation-wide value would be much more work and, hopefully, this
// workaround can be eliminated in the future when webauthn.dll is faster.
//
// Since the `Profile` may be destroyed while the webauthn.dll call is still
// pending, this class observes the profile and handles that event.
class CredentialPresenceCacher : public ProfileObserver {
 public:
  CredentialPresenceCacher(
      Profile* profile,
      base::OnceCallback<void(
          std::optional<std::vector<device::DiscoverableCredentialMetadata>>)>
          callback)
      : profile_(profile), callback_(std::move(callback)) {}

  ~CredentialPresenceCacher() override {
    if (profile_) {
      profile_->RemoveObserver(this);
      profile_ = nullptr;
    }
  }

  void OnEnumerateResult(
      std::vector<device::DiscoverableCredentialMetadata> credentials) {
    if (profile_) {
      profile_->GetPrefs()->SetBoolean(
          kHasPlatformCredentialsPref,
          ContainsUserCreatedCredential(credentials));
    }
    std::sort(credentials.begin(), credentials.end(), CredentialComparator());
    std::move(callback_).Run(std::move(credentials));
  }

  // ProfileObserver:

  void OnProfileWillBeDestroyed(Profile* profile) override {
    DCHECK_EQ(profile, profile_);
    profile_->RemoveObserver(this);
    profile_ = nullptr;
  }

 private:
  raw_ptr<Profile> profile_;
  base::OnceCallback<void(
      std::optional<std::vector<device::DiscoverableCredentialMetadata>>)>
      callback_;
};

void EnumerateResultToBool(
    base::OnceCallback<void(bool)> callback,
    std::optional<std::vector<device::DiscoverableCredentialMetadata>>
        credentials) {
  std::move(callback).Run(credentials.has_value() &&
                          ContainsUserCreatedCredential(*credentials));
}

}  // namespace

LocalCredentialManagementWin::LocalCredentialManagementWin(
    device::WinWebAuthnApi* api,
    Profile* profile)
    : api_(api), profile_(profile) {}

// static
void LocalCredentialManagementWin::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterBooleanPref(kHasPlatformCredentialsPref, false);
}

std::unique_ptr<LocalCredentialManagement> LocalCredentialManagement::Create(
    Profile* profile) {
  return std::make_unique<LocalCredentialManagementWin>(
      device::WinWebAuthnApi::GetDefault(), profile);
}

void LocalCredentialManagementWin::HasCredentials(
    base::OnceCallback<void(bool)> callback) {
  std::optional<bool> result;

  if (!api_->IsAvailable() || !api_->SupportsSilentDiscovery()) {
    result = false;
  } else if (profile_->GetPrefs()->GetBoolean(kHasPlatformCredentialsPref)) {
    result = true;
  }

  if (result.has_value()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), *result));
    return;
  }

  auto cacher = std::make_unique<CredentialPresenceCacher>(
      profile_, base::BindOnce(EnumerateResultToBool, std::move(callback)));
  device::WinWebAuthnApiAuthenticator::EnumeratePlatformCredentials(
      api_, /*rp_id=*/std::nullopt,
      base::BindOnce(&CredentialPresenceCacher::OnEnumerateResult,
                     std::move(cacher)));
}

void LocalCredentialManagementWin::Enumerate(
    base::OnceCallback<void(
        std::optional<std::vector<device::DiscoverableCredentialMetadata>>)>
        callback) {
  if (!api_->IsAvailable() || !api_->SupportsSilentDiscovery()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
    return;
  }

  auto cacher =
      std::make_unique<CredentialPresenceCacher>(profile_, std::move(callback));
  device::WinWebAuthnApiAuthenticator::EnumeratePlatformCredentials(
      api_, /*rp_id=*/std::nullopt,
      base::BindOnce(&CredentialPresenceCacher::OnEnumerateResult,
                     std::move(cacher)));
}

void LocalCredentialManagementWin::Delete(
    base::span<const uint8_t> credential_id,
    base::OnceCallback<void(bool)> callback) {
  device::WinWebAuthnApiAuthenticator::DeletePlatformCredential(
      api_, credential_id, std::move(callback));
}

void LocalCredentialManagementWin::Edit(
    base::span<uint8_t> credential_id,
    std::string new_username,
    base::OnceCallback<void(bool)> callback) {
  // Editing passkeys should not be an option in Windows.
  NOTREACHED();
}
