// 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 "services/device/public/cpp/geolocation/system_geolocation_source_win.h"

#include <windows.foundation.h>
#include <windows.system.h>
#include <wrl/client.h>
#include <wrl/event.h>
#include <wrl/implements.h>

#include <optional>
#include <string_view>

#include "base/memory/raw_ref.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_thread_priority.h"
#include "base/win/core_winrt_util.h"
#include "base/win/post_async_results.h"
#include "services/device/public/cpp/device_features.h"

namespace device {

namespace {

using ::ABI::Windows::Devices::Geolocation::GeolocationAccessStatus;
using ::ABI::Windows::Foundation::ITypedEventHandler;
using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
    AppCapability;
using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
    AppCapabilityAccessChangedEventArgs;
using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
    IAppCapability;
using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
    IAppCapabilityAccessChangedEventArgs;
using ::Microsoft::WRL::ComPtr;

void RecordUmaInitialPermissionStatus(LocationSystemPermissionStatus status) {
  base::UmaHistogramEnumeration(
      "Geolocation.SystemGeolocationSourceWin.InitialPermissionStatus", status);
}

void RecordUmaPermissionStatusChanged(LocationSystemPermissionStatus status,
                                      bool after_prompt) {
  // We don't know what caused the permission status to change. Assume that the
  // first status change after showing the system permission prompt was caused
  // by the user interacting with the prompt.
  if (after_prompt) {
    base::UmaHistogramEnumeration(
        "Geolocation.SystemGeolocationSourceWin."
        "PermissionStatusChangedAfterPrompt",
        status);
  } else {
    base::UmaHistogramEnumeration(
        "Geolocation.SystemGeolocationSourceWin.PermissionStatusChanged",
        status);
  }
}

void RecordUmaCheckAccessError(HRESULT error) {
  base::UmaHistogramSparse(
      "Geolocation.SystemGeolocationSourceWin.CheckAccessError", error);
}

void RecordUmaAddAccessChangedError(HRESULT error) {
  base::UmaHistogramSparse(
      "Geolocation.SystemGeolocationSourceWin.AddAccessChangedError", error);
}

void RecordUmaCreateAppCapabilityError(HRESULT error) {
  base::UmaHistogramSparse(
      "Geolocation.SystemGeolocationSourceWin.CreateAppCapabilityError", error);
}

void RecordUmaLaunchSettingsResult(HRESULT result) {
  base::UmaHistogramSparse(
      "Geolocation.SystemGeolocationSourceWin.LaunchSettingsResult", result);
}

void RecordUmaRequestAccessResult(HRESULT result) {
  base::UmaHistogramSparse(
      "Geolocation.SystemGeolocationSourceWin.RequestAccessResult", result);
}

SystemGeolocationSourceWin::AppCapabilityFactory& GetFactoryStorage() {
  static base::NoDestructor<SystemGeolocationSourceWin::AppCapabilityFactory>
      factory;
  return *factory;
}

// Create an AppCapability object for the capability named `name`.
ComPtr<IAppCapability> CreateAppCapability(std::string_view name) {
  if (GetFactoryStorage()) {
    return GetFactoryStorage().Run(name);
  }

  // COM calls can load DLLs, use background priority to avoid I/O inversion.
  SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();

  using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
      IAppCapabilityStatics;
  ComPtr<IAppCapabilityStatics> app_capability_statics;
  HRESULT hr = base::win::GetActivationFactory<
      IAppCapabilityStatics,
      RuntimeClass_Windows_Security_Authorization_AppCapabilityAccess_AppCapability>(
      &app_capability_statics);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to get IAppCapability statics: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaCreateAppCapabilityError(hr);
    return nullptr;
  }
  auto capability_name = base::win::ScopedHString::Create(name);
  ComPtr<IAppCapability> app_capability;
  hr = app_capability_statics->Create(capability_name.get(), &app_capability);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to create IAppCapability: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaCreateAppCapabilityError(hr);
    return nullptr;
  }
  return app_capability;
}

class AccessChangedHandler
    : public Microsoft::WRL::RuntimeClass<
          Microsoft::WRL::RuntimeClassFlags<
              Microsoft::WRL::ClassicCom |
              Microsoft::WRL::InhibitRoOriginateError>,
          ITypedEventHandler<AppCapability*,
                             AppCapabilityAccessChangedEventArgs*>,
          Microsoft::WRL::FtmBase> {
 public:
  explicit AccessChangedHandler(base::RepeatingClosure on_changed_callback)
      : on_changed_callback_(
            base::BindPostTask(base::SequencedTaskRunner::GetCurrentDefault(),
                               std::move(on_changed_callback))) {}
  AccessChangedHandler(const AccessChangedHandler&) = delete;
  AccessChangedHandler& operator=(const AccessChangedHandler&) = delete;
  ~AccessChangedHandler() override = default;

  IFACEMETHODIMP Invoke(IAppCapability* sender,
                        IAppCapabilityAccessChangedEventArgs* args) override {
    on_changed_callback_.Run();
    return S_OK;
  }

 private:
  base::RepeatingClosure on_changed_callback_;
};

}  // namespace

class SystemGeolocationSourceWin::AccessListenerHelper {
 public:
  AccessListenerHelper(
      base::WeakPtr<SystemGeolocationSourceWin> source,
      scoped_refptr<base::SequencedTaskRunner> source_task_runner);
  AccessListenerHelper(const AccessListenerHelper&) = delete;
  AccessListenerHelper& operator=(const AccessListenerHelper&) = delete;
  ~AccessListenerHelper();

  // This function periodically checks and informs `source_` about the system's
  // geolocation permission status. It handles potential access failures and
  // ensures `source_` is always updated on any permission changes by scheduling
  // regular checks.
  void PollPermissionStatus();

 private:
  void OnAccessChanged();
  // The interval (in milliseconds) at which to poll for permission changes.
  static constexpr int kDefaultPollingIntervalMs = 500;
  const int polling_interval_;
  // COM interface to check the app's capability to access location.
  ComPtr<IAppCapability> location_capability_;
  std::optional<EventRegistrationToken> location_token_;
  base::WeakPtr<SystemGeolocationSourceWin> source_;
  scoped_refptr<base::SequencedTaskRunner> source_task_runner_;
  base::WeakPtrFactory<SystemGeolocationSourceWin::AccessListenerHelper>
      weak_ptr_factory_{this};
};

SystemGeolocationSourceWin::AccessListenerHelper::AccessListenerHelper(
    base::WeakPtr<SystemGeolocationSourceWin> source,
    scoped_refptr<base::SequencedTaskRunner> source_task_runner)
    : polling_interval_(kDefaultPollingIntervalMs),
      location_capability_(CreateAppCapability("location")),
      source_(source),
      source_task_runner_(source_task_runner) {
  // If the location capability object is not available (potentially due
  // to initialization failure or other issues), post a task to notify the
  // SystemGeolocationSourceWin that the permission status is 'kNotDetermined'.
  if (!location_capability_) {
    source_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&SystemGeolocationSourceWin::OnPermissionStatusUpdated,
                       source_,
                       LocationSystemPermissionStatus::kNotDetermined));
    return;
  }

  if (base::FeatureList::IsEnabled(
          features::kWinSystemLocationPermissionEventBased)) {
    HRESULT hr = location_capability_->add_AccessChanged(
        Microsoft::WRL::Make<AccessChangedHandler>(
            base::BindRepeating(&AccessListenerHelper::OnAccessChanged,
                                weak_ptr_factory_.GetWeakPtr()))
            .Get(),
        &location_token_.emplace());
    if (FAILED(hr)) {
      LOG(ERROR) << "Failed to add access change listener: "
                 << logging::SystemErrorCodeToString(hr);
      RecordUmaAddAccessChangedError(hr);
      location_token_.reset();
    }
  }

  PollPermissionStatus();
}

SystemGeolocationSourceWin::AccessListenerHelper::~AccessListenerHelper() {
  if (location_capability_ && location_token_) {
    location_capability_->remove_AccessChanged(*location_token_);
  }
}

void SystemGeolocationSourceWin::AccessListenerHelper::OnAccessChanged() {
  PollPermissionStatus();
}

void SystemGeolocationSourceWin::AccessListenerHelper::PollPermissionStatus() {
  using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
      AppCapabilityAccessStatus;
  using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
      AppCapabilityAccessStatus_Allowed;
  using ::ABI::Windows::Security::Authorization::AppCapabilityAccess::
      AppCapabilityAccessStatus_UserPromptRequired;

  LocationSystemPermissionStatus status =
      LocationSystemPermissionStatus::kDenied;
  AppCapabilityAccessStatus access_status;
  HRESULT hr = location_capability_->CheckAccess(&access_status);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to get location access status: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaCheckAccessError(hr);
    status = LocationSystemPermissionStatus::kNotDetermined;
  } else if (access_status == AppCapabilityAccessStatus_Allowed) {
    status = LocationSystemPermissionStatus::kAllowed;
  } else if (access_status == AppCapabilityAccessStatus_UserPromptRequired) {
    status = LocationSystemPermissionStatus::kNotDetermined;
  }

  source_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&SystemGeolocationSourceWin::OnPermissionStatusUpdated,
                     source_, status));

  // If we have a token, we are using events and don't need to poll.
  if (location_token_) {
    return;
  }

  // Schedule next poll.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&SystemGeolocationSourceWin::AccessListenerHelper::
                         PollPermissionStatus,
                     weak_ptr_factory_.GetWeakPtr()),
      base::Milliseconds(polling_interval_));
}

SystemGeolocationSourceWin::SystemGeolocationSourceWin() {
  access_listener_helper_ = base::SequenceBound<AccessListenerHelper>(
      base::ThreadPool::CreateCOMSTATaskRunner(
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT}),
      weak_factory_.GetWeakPtr(),
      base::SequencedTaskRunner::GetCurrentDefault());
}

SystemGeolocationSourceWin::~SystemGeolocationSourceWin() = default;

// static
std::unique_ptr<GeolocationSystemPermissionManager>
SystemGeolocationSourceWin::CreateGeolocationSystemPermissionManager() {
  if (!features::IsOsLevelGeolocationPermissionSupportEnabled()) {
    return nullptr;
  }
  return std::make_unique<GeolocationSystemPermissionManager>(
      std::make_unique<SystemGeolocationSourceWin>());
}

// static
void SystemGeolocationSourceWin::SetAppCapabilityFactoryForTesting(
    AppCapabilityFactory factory) {
  GetFactoryStorage() = std::move(factory);
}

void SystemGeolocationSourceWin::RegisterPermissionUpdateCallback(
    PermissionUpdateCallback callback) {
  permission_update_callback_ = std::move(callback);
  if (permission_status_.has_value()) {
    permission_update_callback_.Run(permission_status_.value());
  }
}

void SystemGeolocationSourceWin::OnPermissionStatusUpdated(
    LocationSystemPermissionStatus status) {
  // Record the first polled permission status.
  if (!permission_status_.has_value()) {
    RecordUmaInitialPermissionStatus(status);
  }

  // Handle permission status change.
  if (status != permission_status_) {
    permission_status_ = status;
    if (permission_update_callback_) {
      permission_update_callback_.Run(status);
    }
    RecordUmaPermissionStatusChanged(status, has_pending_system_prompt_);
    has_pending_system_prompt_ = false;
  }
}

void SystemGeolocationSourceWin::OnLaunchUriSuccess(uint8_t launched) {
  RecordUmaLaunchSettingsResult(S_OK);
  launch_uri_op_.Reset();
}

void SystemGeolocationSourceWin::OnLaunchUriFailure(HRESULT result) {
  LOG(ERROR) << "LaunchUriAsync failed: "
             << logging::SystemErrorCodeToString(result);
  RecordUmaLaunchSettingsResult(result);
  launch_uri_op_.Reset();
}

void SystemGeolocationSourceWin::OpenSystemPermissionSetting() {
  using ::ABI::Windows::Foundation::IUriRuntimeClass;
  using ::ABI::Windows::Foundation::IUriRuntimeClassFactory;
  using ::ABI::Windows::System::ILauncherStatics;
  if (launch_uri_op_) {
    return;
  }
  ComPtr<IUriRuntimeClassFactory> uri_runtime_class_factory;
  HRESULT hr =
      base::win::GetActivationFactory<IUriRuntimeClassFactory,
                                      RuntimeClass_Windows_Foundation_Uri>(
          &uri_runtime_class_factory);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to get IUriRuntimeClassFactory: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaLaunchSettingsResult(hr);
    return;
  }
  ComPtr<IUriRuntimeClass> uri_runtime_class;
  auto uri_string =
      base::win::ScopedHString::Create("ms-settings:privacy-location");
  hr = uri_runtime_class_factory->CreateUri(uri_string.get(),
                                            &uri_runtime_class);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to create IUriRuntimeClass: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaLaunchSettingsResult(hr);
    return;
  }
  ComPtr<ILauncherStatics> launcher_statics;
  hr = base::win::GetActivationFactory<ILauncherStatics,
                                       RuntimeClass_Windows_System_Launcher>(
      &launcher_statics);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to get ILauncher statics: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaLaunchSettingsResult(hr);
    return;
  }
  hr = launcher_statics->LaunchUriAsync(uri_runtime_class.Get(),
                                        &launch_uri_op_);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to launch URI: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaLaunchSettingsResult(hr);
    return;
  }
  hr = base::win::PostAsyncHandlers(
      launch_uri_op_.Get(),
      base::BindOnce(&SystemGeolocationSourceWin::OnLaunchUriSuccess,
                     weak_factory_.GetWeakPtr()),
      base::BindOnce(&SystemGeolocationSourceWin::OnLaunchUriFailure,
                     weak_factory_.GetWeakPtr()));
  if (FAILED(hr)) {
    LOG(ERROR) << "PostAsyncHandlers failed: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaLaunchSettingsResult(hr);
  }
}

void SystemGeolocationSourceWin::RequestPermission() {
  using ::ABI::Windows::Devices::Geolocation::IGeolocatorStatics;
  if (request_location_access_op_) {
    return;
  }
  ComPtr<IGeolocatorStatics> geolocator_statics;
  HRESULT hr = base::win::GetActivationFactory<
      IGeolocatorStatics, RuntimeClass_Windows_Devices_Geolocation_Geolocator>(
      &geolocator_statics);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to get IGeolocator statics: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaRequestAccessResult(hr);
    return;
  }
  // Geolocator::RequestAccessAsync will trigger the one-time-per-app prompt.
  hr = geolocator_statics->RequestAccessAsync(&request_location_access_op_);
  if (FAILED(hr)) {
    LOG(ERROR) << "Location access request failed: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaRequestAccessResult(hr);
    return;
  }
  hr = base::win::PostAsyncHandlers(
      request_location_access_op_.Get(),
      base::BindOnce(
          &SystemGeolocationSourceWin::OnRequestLocationAccessSuccess,
          weak_factory_.GetWeakPtr()),
      base::BindOnce(
          &SystemGeolocationSourceWin::OnRequestLocationAccessFailure,
          weak_factory_.GetWeakPtr()));
  if (FAILED(hr)) {
    LOG(ERROR) << "PostAsyncHandlers failed: "
               << logging::SystemErrorCodeToString(hr);
    RecordUmaRequestAccessResult(hr);
    return;
  }
  has_pending_system_prompt_ = true;
}

void SystemGeolocationSourceWin::OnRequestLocationAccessSuccess(
    GeolocationAccessStatus status) {
  RecordUmaRequestAccessResult(S_OK);
  request_location_access_op_.Reset();
}

void SystemGeolocationSourceWin::OnRequestLocationAccessFailure(
    HRESULT result) {
  LOG(ERROR) << "RequestLocationAccess failed: "
             << logging::SystemErrorCodeToString(result);
  RecordUmaRequestAccessResult(result);
  request_location_access_op_.Reset();
}

}  // namespace device
