// Copyright 2014 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/ash/policy/enrollment/auto_enrollment_controller.h"

#include <memory>

#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/login/oobe_configuration.h"
#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
#include "chrome/browser/ash/policy/enrollment/auto_enrollment_state.h"
#include "chrome/browser/ash/policy/enrollment/auto_enrollment_type_checker.h"
#include "chrome/browser/ash/policy/enrollment/enrollment_state_fetcher.h"
#include "chrome/browser/ash/policy/enrollment/psm/rlwe_dmserver_client_impl.h"
#include "chrome/browser/ash/policy/server_backed_state/server_backed_state_keys_broker.h"
#include "chrome/browser/ash/settings/device_settings_service.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chromeos/ash/components/dbus/session_manager/session_manager_client.h"
#include "chromeos/ash/components/install_attributes/install_attributes.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "components/policy/core/common/cloud/device_management_service.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

// This is used for logs that may not be strictly necessary but are of great use
// because they will log whether determinations are needed or not, along with
// some context. The information used to be logged using VLOG(1), and therefore
// was not available in customer logs. Because the only other logs have some
// ambiguity (e.g. there will not be a log if the device decides it does not
// need to make a determination), troubleshooting is difficult. If this changes,
// this can be made VLOG(1) again.
//
// We use LOG(WARNING) to guarantee that the messages will be into feedback
// reports.
#define LOG_DETERMINATION() LOG(WARNING)

namespace policy {
namespace {

// Maximum time to wait for the auto-enrollment check to reach a decision.
// Note that this encompasses all steps `AutoEnrollmentController` performs in
// order to determine if the device should be auto-enrolled.
// If `kSafeguardTimeout` after `Start()` has been called,
// `AutoEnrollmentController::state()` is still AutoEnrollmentState::kPending,
// the AutoEnrollmentController will switch to
// `AutoEnrollmentResult::kNoEnrollment` or
// `AutoEnrollmentSafeguardTimeoutError` (see
// `AutoEnrollmentController::Timeout`). Note that this timeout should not be
// too short, because one of the steps `AutoEnrollmentController` performs -
// downloading identifier hash buckets - can be non-negligible, especially on 2G
// connections.
constexpr base::TimeDelta kSafeguardTimeout = base::Seconds(90);

enum class AutoEnrollmentControllerTimeoutReport {
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  kTimeoutCancelled = 0,
  // kTimeoutFRE_Obsolete = 1,
  kTimeout = 2,
  kTimeoutUnified = 3,
  kMaxValue = kTimeoutUnified
};

void ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport report) {
  base::UmaHistogramEnumeration("Enterprise.AutoEnrollmentControllerTimeout",
                                report);
}

bool IsFinalAutoEnrollmentState(AutoEnrollmentState state) {
  return state.has_value();
}

}  // namespace

AutoEnrollmentController::AutoEnrollmentController(
    PrefService* local_state,
    scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
    DeviceManagementService* device_management_service,
    ServerBackedStateKeysBroker* state_keys_broker)
    : AutoEnrollmentController(
          local_state,
          std::move(shared_url_loader_factory),
          ash::DeviceSettingsService::Get(),
          device_management_service,
          state_keys_broker,
          ash::NetworkHandler::Get()->network_state_handler(),
          base::BindRepeating(&policy::psm::RlweDmserverClientImpl::Create),
          base::BindRepeating(EnrollmentStateFetcher::Create)) {}

AutoEnrollmentController::AutoEnrollmentController(
    PrefService* local_state,
    scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
    ash::DeviceSettingsService* device_settings_service,
    DeviceManagementService* device_management_service,
    ServerBackedStateKeysBroker* state_keys_broker,
    ash::NetworkStateHandler* network_state_handler,
    RlweClientFactory psm_rlwe_client_factory,
    EnrollmentStateFetcher::Factory enrollment_state_fetcher_factory)
    : local_state_(CHECK_DEREF(local_state)),
      shared_url_loader_factory_(std::move(shared_url_loader_factory)),
      device_settings_service_(device_settings_service),
      device_management_service_(device_management_service),
      state_keys_broker_(state_keys_broker),
      psm_rlwe_client_factory_(std::move(psm_rlwe_client_factory)),
      enrollment_state_fetcher_factory_(
          std::move(enrollment_state_fetcher_factory)),
      network_state_handler_(network_state_handler) {
  CHECK(shared_url_loader_factory_);
  if (!device_management_service_ || !state_keys_broker_) {
    CHECK_IS_TEST();
  }
}

AutoEnrollmentController::~AutoEnrollmentController() = default;

void AutoEnrollmentController::Start() {
  if (state_.has_value() && IsFinalAutoEnrollmentState(state_.value())) {
    return;
  }

  if (!network_state_observation_.IsObserving()) {
    // The controller could have already subscribed on the start and now we're
    // restarting after an error.
    network_state_observation_.Observe(network_state_handler_);
  }

  if (IsInProgress()) {
    return;
  }

  // If Unified State Determination is turned off (e.g. by command line switch),
  // then enrollment will not be forced.
  if (!AutoEnrollmentTypeChecker::IsUnifiedStateDeterminationEnabled()) {
    LOG(WARNING) << "State determination not enabled";
    UpdateState(AutoEnrollmentResult::kNoEnrollment);
    return;
  }

  // Arm the belts-and-suspenders timer to avoid hangs.
  safeguard_timer_.Start(FROM_HERE, kSafeguardTimeout,
                         base::BindOnce(&AutoEnrollmentController::Timeout,
                                        weak_ptr_factory_.GetWeakPtr()));

  // TODO(crbug.com/353731379):
  // BrowserPolicyConnector::ScheduleServiceInitialization.
  if (device_management_service_) {
    device_management_service_->ScheduleInitialization(0);
  } else {
    CHECK_IS_TEST();
  }

  LOG(WARNING) << "Starting state determination";
  enrollment_state_fetcher_ = enrollment_state_fetcher_factory_.Run(
      base::BindRepeating(&AutoEnrollmentController::UpdateState,
                          weak_ptr_factory_.GetWeakPtr()),
      &local_state_.get(), psm_rlwe_client_factory_, device_management_service_,
      shared_url_loader_factory_, state_keys_broker_, device_settings_service_,
      ash::OobeConfiguration::Get());

  enrollment_state_fetcher_->Start();
  return;
}

base::CallbackListSubscription
AutoEnrollmentController::RegisterProgressCallback(
    const ProgressCallbackList::CallbackType& callback) {
  return progress_callbacks_.Add(callback);
}

void AutoEnrollmentController::PortalStateChanged(
    const ash::NetworkState* /*default_network*/,
    const ash::NetworkState::PortalState portal_state) {
  // It is safe to retry regardless of the current state: if the check is idle
  // or failed, we will restart the check process. If the check is in progress,
  // the retry call will be ignored.
  if (portal_state == ash::NetworkState::PortalState::kOnline) {
    Start();
  }
}

void AutoEnrollmentController::OnShuttingDown() {
  network_state_observation_.Reset();
}

void AutoEnrollmentController::SetRlweClientFactoryForTesting(
    RlweClientFactory test_factory) {
  CHECK_IS_TEST();
  psm_rlwe_client_factory_ = std::move(test_factory);
}

void AutoEnrollmentController::UpdateState(AutoEnrollmentState new_state) {
  LOG(WARNING) << "New auto-enrollment state: "
               << AutoEnrollmentStateToString(new_state);
  state_ = new_state;

  if (state_ == AutoEnrollmentResult::kDeviceAlreadyOwned) {
    LOG(ERROR) << "Device already owned. Powerwash required.";
    ash::SessionManagerClient::Get()->StartDeviceWipe(base::DoNothing());

    // Will trigger timeout after a while in case powerwash should fail for
    // whatever reason.
    return;
  }

  if (IsFinalAutoEnrollmentState(state_.value())) {
    network_state_observation_.Reset();
  }

  // Stop the safeguard timer once a result comes in.
  safeguard_timer_.Stop();
  // Reset enrollment state fetcher to allow restarting.
  enrollment_state_fetcher_.reset();
  ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport::kTimeoutCancelled);

  // Device disabling mode is relying on device state stored in install
  // attributes. In case that file is corrupted, this should prevent device
  // re-enabling.
  if (state_ == AutoEnrollmentResult::kDisabled) {
    DeviceMode device_mode = ash::InstallAttributes::Get()->GetMode();
    if (device_mode == DeviceMode::DEVICE_MODE_PENDING ||
        device_mode == DeviceMode::DEVICE_MODE_NOT_SET) {
      device_settings_service_->SetDeviceMode(
          DeviceMode::DEVICE_MODE_ENTERPRISE);
    }
  }

  if (state_ == AutoEnrollmentResult::kNoEnrollment ||
      state_ == AutoEnrollmentResult::kSuggestedEnrollment) {
    StartCleanupForcedReEnrollment();
  } else {
    progress_callbacks_.Notify(state_.value());
  }
}

void AutoEnrollmentController::StartCleanupForcedReEnrollment() {
  // D-Bus services may not be available yet, so we call
  // WaitForServiceToBeAvailable. See https://crbug.com/40575725.
  ash::InstallAttributesClient::Get()->WaitForServiceToBeAvailable(
      base::BindOnce(
          &AutoEnrollmentController::StartRemoveFirmwareManagementParameters,
          weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::StartRemoveFirmwareManagementParameters(
    bool service_is_ready) {
  DCHECK(state_ == AutoEnrollmentResult::kNoEnrollment ||
         state_ == AutoEnrollmentResult::kSuggestedEnrollment);
  if (!service_is_ready) {
    LOG(ERROR) << "Failed waiting for cryptohome D-Bus service availability.";
    progress_callbacks_.Notify(state_.value());
    return;
  }

  device_management::RemoveFirmwareManagementParametersRequest request;
  ash::InstallAttributesClient::Get()->RemoveFirmwareManagementParameters(
      request,
      base::BindOnce(
          &AutoEnrollmentController::OnFirmwareManagementParametersRemoved,
          weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::OnFirmwareManagementParametersRemoved(
    std::optional<device_management::RemoveFirmwareManagementParametersReply>
        reply) {
  if (!reply.has_value() || reply->error() !=
                                device_management::DeviceManagementErrorCode::
                                    DEVICE_MANAGEMENT_ERROR_NOT_SET) {
    LOG(ERROR) << "Failed to remove firmware management parameters.";
  }

  // D-Bus services may not be available yet, so we call
  // WaitForServiceToBeAvailable. See https://crbug.com/40575725.
  ash::SessionManagerClient::Get()->WaitForServiceToBeAvailable(
      base::BindOnce(&AutoEnrollmentController::StartClearBlockDevmodeVpd,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::StartClearBlockDevmodeVpd(
    bool service_is_ready) {
  DCHECK(state_ == AutoEnrollmentResult::kNoEnrollment ||
         state_ == AutoEnrollmentResult::kSuggestedEnrollment);
  if (!service_is_ready) {
    LOG(ERROR)
        << "Failed waiting for session_manager D-Bus service availability.";
    progress_callbacks_.Notify(state_.value());
    return;
  }

  // This clears block_devmode in the VPD.
  ash::SessionManagerClient::Get()->ClearBlockDevmodeVpd(
      base::BindOnce(&AutoEnrollmentController::OnBlockDevmodeClearedVpd,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::OnBlockDevmodeClearedVpd(bool succeeded) {
  if (!succeeded) {
    LOG(ERROR) << "Failed to clear forced re-enrollment flags in RW VPD.";
  }

  progress_callbacks_.Notify(state_.value());
}

void AutoEnrollmentController::Timeout() {
  // The timer is only started if Unified State Determination is enabled.
  CHECK(AutoEnrollmentTypeChecker::IsUnifiedStateDeterminationEnabled());

  // This can actually happen in some cases, for example when state key
  // generation is waiting for time sync or the server just doesn't reply and
  // keeps the connection open.
  LOG(ERROR) << "EnrollmentStateFetcher didn't complete within time limit.";
  UpdateState(base::unexpected(AutoEnrollmentSafeguardTimeoutError{}));
  ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport::kTimeoutUnified);
}

bool AutoEnrollmentController::IsInProgress() const {
  if (AutoEnrollmentTypeChecker::IsUnifiedStateDeterminationEnabled()) {
    if (enrollment_state_fetcher_) {
      // If a fetcher has already been created, bail out.
      VLOG(1) << "Enrollment state fetcher is already running.";
      return true;
    }
  }

  return false;
}

void AutoEnrollmentController::SetEnrollmentStateFetcherFactoryForTesting(
    EnrollmentStateFetcher::Factory enrollment_state_fetcher_factory) {
  CHECK_IS_TEST();
  if (enrollment_state_fetcher_factory) {
    enrollment_state_fetcher_factory_ = enrollment_state_fetcher_factory;
  } else {
    enrollment_state_fetcher_factory_ =
        base::BindRepeating(EnrollmentStateFetcher::Create);
  }
}

bool AutoEnrollmentController::IsGuestSigninAllowed() const {
  return state_ == AutoEnrollmentResult::kNoEnrollment ||
         state_ == AutoEnrollmentResult::kSuggestedEnrollment;
}

}  // namespace policy
