// Copyright 2017 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/ui/webui/bluetooth_internals/bluetooth_internals_handler.h"

#include <fcntl.h>

#include <optional>
#include <string>

#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "device/bluetooth/adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/public/cpp/bluetooth_config_service.h"
#include "chrome/browser/ash/bluetooth/debug_logs_manager.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "device/bluetooth/chromeos_platform_features.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_ANDROID)
#include "components/permissions/android/android_permission_util.h"
#endif  // BUILDFLAG(IS_ANDROID)

namespace {
using content::RenderFrameHost;
using content::WebContents;

#if BUILDFLAG(IS_CHROMEOS)
using ash::bluetooth_config::mojom::BluetoothSystemState;

constexpr char kBtsnoopFileName[] = "capture.btsnoop";
// TODO(b/347880605): find a better way to query the actual downloads path
constexpr char kDownloadsPath[] = "/home/chronos/user/Downloads/";
constexpr char kDownloadsPathNew[] = "/home/chronos/user/MyFiles/Downloads/";

class BluetoothBtsnoop : public mojom::BluetoothBtsnoop {
 public:
  explicit BluetoothBtsnoop(
      mojo::PendingReceiver<mojom::BluetoothBtsnoop> receiver,
      base::OnceCallback<void(StopCallback callback)> on_stop_callback)
      : receiver_(this, std::move(receiver)),
        on_stop_callback_(std::move(on_stop_callback)) {}

  BluetoothBtsnoop(const BluetoothBtsnoop&) = delete;
  BluetoothBtsnoop& operator=(const BluetoothBtsnoop&) = delete;
  ~BluetoothBtsnoop() override = default;

  void Stop(StopCallback callback) override;

 private:
  mojo::Receiver<mojom::BluetoothBtsnoop> receiver_;
  base::OnceCallback<void(StopCallback callback)> on_stop_callback_;
};
#endif  // BUILDFLAG(IS_CHROMEOS)
}  // namespace

BluetoothInternalsHandler::BluetoothInternalsHandler(
    content::RenderFrameHost* render_frame_host,
    mojo::PendingReceiver<mojom::BluetoothInternalsHandler> receiver)
    : render_frame_host_(*render_frame_host),
      receiver_(this, std::move(receiver)) {
#if BUILDFLAG(IS_CHROMEOS)
  ash::GetBluetoothConfigService(
      remote_cros_bluetooth_config_.BindNewPipeAndPassReceiver());
  remote_cros_bluetooth_config_->ObserveSystemProperties(
      cros_system_properties_observer_receiver_.BindNewPipeAndPassRemote());
#endif  // BUILDFLAG(IS_CHROMEOS)
}

BluetoothInternalsHandler::~BluetoothInternalsHandler() {
#if BUILDFLAG(IS_CHROMEOS)
  if (!turning_bluetooth_off_ && !turning_bluetooth_on_) {
    return;
  }

  remote_cros_bluetooth_config_->SetBluetoothEnabledState(
      bluetooth_initial_state_);
#endif  // BUILDFLAG(IS_CHROMEOS)
}

void BluetoothInternalsHandler::GetAdapter(GetAdapterCallback callback) {
  if (device::BluetoothAdapterFactory::IsBluetoothSupported()) {
    device::BluetoothAdapterFactory::Get()->GetAdapter(
        base::BindOnce(&BluetoothInternalsHandler::OnGetAdapter,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  } else {
    std::move(callback).Run(mojo::NullRemote() /* adapter */);
  }
}

void BluetoothInternalsHandler::GetDebugLogsChangeHandler(
    GetDebugLogsChangeHandlerCallback callback) {
  mojo::PendingRemote<mojom::DebugLogsChangeHandler> handler_remote;
  bool initial_toggle_value = false;

#if BUILDFLAG(IS_CHROMEOS)
  using ash::bluetooth::DebugLogsManager;

  // If no logs manager exists for this user, debug logs are not supported.
  DebugLogsManager::DebugLogsState state =
      debug_logs_manager_ ? debug_logs_manager_->GetDebugLogsState()
                          : DebugLogsManager::DebugLogsState::kNotSupported;

  switch (state) {
    case DebugLogsManager::DebugLogsState::kNotSupported:
      // Leave |handler_remote| NullRemote and |initial_toggle_value| false.
      break;
    case DebugLogsManager::DebugLogsState::kSupportedAndEnabled:
      initial_toggle_value = true;
      [[fallthrough]];
    case DebugLogsManager::DebugLogsState::kSupportedButDisabled:
      handler_remote = debug_logs_manager_->GenerateRemote();
      break;
  }
#endif

  std::move(callback).Run(std::move(handler_remote), initial_toggle_value);
}

void BluetoothInternalsHandler::OnGetAdapter(
    GetAdapterCallback callback,
    scoped_refptr<device::BluetoothAdapter> adapter) {
  mojo::PendingRemote<bluetooth::mojom::Adapter> pending_adapter;
  mojo::MakeSelfOwnedReceiver(std::make_unique<bluetooth::Adapter>(adapter),
                              pending_adapter.InitWithNewPipeAndPassReceiver());
  std::move(callback).Run(std::move(pending_adapter));
}

void BluetoothInternalsHandler::CheckSystemPermissions(
    CheckSystemPermissionsCallback callback) {
  bool need_location_permission = false;
  bool need_nearby_devices_permission = false;
  bool need_location_services = false;
  bool can_request_system_permissions = false;

#if BUILDFLAG(IS_ANDROID)
  WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(&render_frame_host_.get());
  need_location_permission =
      permissions::NeedsLocationPermissionForBluetooth(web_contents);
  need_nearby_devices_permission =
      permissions::NeedsNearbyDevicesPermissionForBluetooth(web_contents);
  need_location_services = permissions::NeedsLocationServicesForBluetooth();
  can_request_system_permissions =
      permissions::CanRequestSystemPermissionsForBluetooth(web_contents);
#endif  // BUILDFLAG(IS_ANDROID)

  std::move(callback).Run(
      need_location_permission, need_nearby_devices_permission,
      need_location_services, can_request_system_permissions);
}

void BluetoothInternalsHandler::RequestSystemPermissions(
    RequestSystemPermissionsCallback callback) {
#if BUILDFLAG(IS_ANDROID)
  WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(&render_frame_host_.get());
  permissions::RequestSystemPermissionsForBluetooth(web_contents);
#endif  // BUILDFLAG(IS_ANDROID)
  std::move(callback).Run();
}

void BluetoothInternalsHandler::RequestLocationServices(
    RequestLocationServicesCallback callback) {
#if BUILDFLAG(IS_ANDROID)
  WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(&render_frame_host_.get());
  permissions::RequestLocationServices(web_contents);
#endif  // BUILDFLAG(IS_ANDROID)
  std::move(callback).Run();
}

#if BUILDFLAG(IS_CHROMEOS)
void BluetoothInternalsHandler::RestartSystemBluetooth(
    RestartSystemBluetoothCallback callback) {
  if (bluetooth_system_state_ == BluetoothSystemState::kUnavailable) {
    std::move(callback).Run();
    return;
  }

  restart_system_bluetooth_callback_ = std::move(callback);
  bool enable = false;
  turning_bluetooth_off_ = true;

  if (bluetooth_system_state_ == BluetoothSystemState::kDisabled ||
      bluetooth_system_state_ == BluetoothSystemState::kDisabling) {
    enable = true;
    turning_bluetooth_on_ = true;
    turning_bluetooth_off_ = false;
  }

  bluetooth_initial_state_ = !enable;
  remote_cros_bluetooth_config_->SetBluetoothEnabledState(enable);
}

void BluetoothInternalsHandler::OnPropertiesUpdated(
    ash::bluetooth_config::mojom::BluetoothSystemPropertiesPtr properties) {
  bluetooth_system_state_ = properties->system_state;
  if (bluetooth_system_state_ == BluetoothSystemState::kUnavailable) {
    return;
  }

  if (!turning_bluetooth_off_ && !turning_bluetooth_on_) {
    return;
  }

  if (bluetooth_system_state_ == BluetoothSystemState::kDisabling ||
      bluetooth_system_state_ == BluetoothSystemState::kEnabling) {
    return;
  }

  CHECK(restart_system_bluetooth_callback_);
  if (bluetooth_system_state_ == BluetoothSystemState::kDisabled &&
      turning_bluetooth_off_) {
    turning_bluetooth_off_ = false;
    turning_bluetooth_on_ = true;
    remote_cros_bluetooth_config_->SetBluetoothEnabledState(true);
  }

  if (bluetooth_system_state_ == BluetoothSystemState::kEnabled &&
      turning_bluetooth_on_) {
    turning_bluetooth_on_ = false;
    turning_bluetooth_off_ = false;
    std::move(restart_system_bluetooth_callback_).Run();
  }
}
#endif  // BUILDFLAG(IS_CHROMEOS)

void BluetoothInternalsHandler::StartBtsnoop(StartBtsnoopCallback callback) {
#if BUILDFLAG(IS_CHROMEOS)
  ash::DebugDaemonClient::Get()->BluetoothStartBtsnoop(
      base::BindOnce(&BluetoothInternalsHandler::OnStartBtsnoopResp,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
#else
  std::move(callback).Run(mojo::NullRemote());
#endif
}

void BluetoothInternalsHandler::IsBtsnoopFeatureEnabled(
    IsBtsnoopFeatureEnabledCallback callback) {
#if BUILDFLAG(IS_CHROMEOS)
  bool enabled = base::FeatureList::IsEnabled(
      chromeos::bluetooth::features::kBluetoothBtsnoopInternals);
  std::move(callback).Run(enabled);
#else
  std::move(callback).Run(false);
#endif
}

#if BUILDFLAG(IS_CHROMEOS)
void BluetoothInternalsHandler::OnStartBtsnoopResp(
    StartBtsnoopCallback callback,
    bool success) {
  if (!success) {
    std::move(callback).Run(mojo::NullRemote());
    return;
  }

  mojo::PendingRemote<mojom::BluetoothBtsnoop> remote_btsnoop;
  btsnoop_ = std::make_unique<BluetoothBtsnoop>(
      remote_btsnoop.InitWithNewPipeAndPassReceiver(),
      base::BindOnce(&BluetoothInternalsHandler::StopBtsnoop,
                     base::Unretained(this)));
  std::move(callback).Run(std::move(remote_btsnoop));
}

void BluetoothInternalsHandler::StopBtsnoop(
    mojom::BluetoothBtsnoop::StopCallback callback) {
  std::optional<base::FilePath> dir_path = GetDownloadsPath();
  if (!dir_path.has_value()) {
    std::move(callback).Run(false);
    return;
  }

  // Open the file for writing, then pass the fd via DBus.
  base::FilePath file_path = dir_path.value().Append(kBtsnoopFileName);
  base::ScopedFD fd(HANDLE_EINTR(
      open(file_path.value().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644)));
  if (!fd.is_valid()) {
    std::move(callback).Run(false);
    return;
  }

  ash::DebugDaemonClient::Get()->BluetoothStopBtsnoop(
      fd.get(),
      base::BindOnce(&BluetoothInternalsHandler::OnStopBtsnoopResp,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void BluetoothInternalsHandler::OnStopBtsnoopResp(
    mojom::BluetoothBtsnoop::StopCallback callback,
    bool success) {
  std::move(callback).Run(success);
  btsnoop_ = nullptr;
}

std::optional<base::FilePath> BluetoothInternalsHandler::GetDownloadsPath() {
  // Check for both downloads paths. There ought to be a better way to do this,
  // but we can iterate later.
  base::FilePath dir_path = base::FilePath(kDownloadsPathNew);
  if (!base::PathExists(dir_path)) {
    dir_path = base::FilePath(kDownloadsPath);
    if (!base::PathExists(dir_path)) {
      return std::nullopt;
    }
  }
  return std::optional<base::FilePath>(dir_path);
}

void BluetoothBtsnoop::Stop(StopCallback callback) {
  std::move(on_stop_callback_).Run(std::move(callback));
}
#endif  // BUILDFLAG(IS_CHROMEOS)
