// Copyright 2018 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/enrollment_config.h"

#include <optional>
#include <string>
#include <string_view>

#include "ash/constants/ash_login_pref_names.h"
#include "ash/constants/ash_policy_pref_names.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/values.h"
#include "chrome/browser/ash/login/configuration_keys.h"
#include "chrome/browser/ash/login/oobe_configuration.h"
#include "chrome/browser/ash/login/wizard_context.h"
#include "chrome/browser/ash/policy/enrollment/enrollment_token_provider.h"
#include "chrome/browser/ash/policy/server_backed_state/server_backed_device_state.h"
#include "chrome/browser/ash/settings/device_settings_service.h"
#include "chrome/browser/ui/ash/login/login_display_host.h"
#include "chromeos/ash/components/install_attributes/install_attributes.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "components/policy/core/common/cloud/cloud_policy_constants.h"
#include "components/prefs/pref_service.h"

namespace policy {
namespace {

std::string GetString(const base::DictValue& dict, std::string_view key) {
  const std::string* value = dict.FindString(key);
  return value ? *value : std::string();
}

bool IsEnrollingAfterRollback() {
  auto* login_display_host = ash::LoginDisplayHost::default_host();
  if (!login_display_host) {
    return false;
  }
  const auto* wizard_context = login_display_host->GetWizardContext();
  return wizard_context && ash::IsRollbackFlow(*wizard_context);
}

// Returns the license type to use based on the license type, assigned
// upgrade type and the license packaged from device state.
LicenseType GetLicenseTypeToUse(const std::string& license_type,
                                bool is_license_packaged_with_device,
                                const std::string& assigned_upgrade_type) {
  if (license_type == kDeviceStateLicenseTypeEnterprise) {
    return LicenseType::kEnterprise;
  }
  if (license_type == kDeviceStateLicenseTypeEducation) {
    return LicenseType::kEducation;
  }
  if (license_type == kDeviceStateLicenseTypeTerminal) {
    return LicenseType::kTerminal;
  }
  if (!is_license_packaged_with_device &&
      assigned_upgrade_type == kDeviceStateAssignedUpgradeTypeKiosk) {
    return LicenseType::kTerminal;
  }
  return LicenseType::kNone;
}

std::string_view ToStringView(EnrollmentConfig::Mode mode) {
#define CASE(_name)                   \
  case EnrollmentConfig::Mode::_name: \
    return #_name;

  switch (mode) {
    CASE(MODE_NONE);
    CASE(MODE_MANUAL);
    CASE(MODE_MANUAL_REENROLLMENT);
    CASE(MODE_LOCAL_FORCED);
    CASE(MODE_LOCAL_ADVERTISED);
    CASE(MODE_SERVER_FORCED);
    CASE(MODE_SERVER_ADVERTISED);
    CASE(MODE_RECOVERY);
    CASE(MODE_ATTESTATION);
    CASE(MODE_ATTESTATION_LOCAL_FORCED);
    CASE(MODE_ATTESTATION_SERVER_FORCED);
    CASE(MODE_ATTESTATION_MANUAL_FALLBACK);
    CASE(MODE_INITIAL_SERVER_FORCED);
    CASE(MODE_ATTESTATION_INITIAL_SERVER_FORCED);
    CASE(MODE_ATTESTATION_INITIAL_MANUAL_FALLBACK);
    CASE(MODE_ATTESTATION_ROLLBACK_FORCED);
    CASE(MODE_ATTESTATION_ROLLBACK_MANUAL_FALLBACK);
    CASE(MODE_ENROLLMENT_TOKEN_INITIAL_SERVER_FORCED);
    CASE(MODE_ENROLLMENT_TOKEN_INITIAL_MANUAL_FALLBACK);
    CASE(MODE_REMOTE_DEPLOYMENT_SERVER_FORCED);
    CASE(MODE_REMOTE_DEPLOYMENT_MANUAL_FALLBACK);
  }

  NOTREACHED();
#undef CASE
}

std::optional<EnrollmentConfig> GetPrescribedRecoveryConfig(
    PrefService& local_state,
    const ash::InstallAttributes& install_attributes,
    ash::system::StatisticsProvider* statistics_provider) {
  if (!local_state.GetBoolean(ash::prefs::kEnrollmentRecoveryRequired)) {
    return std::nullopt;
  }

  if (ash::DeviceSettingsService::IsInitialized() &&
      ash::DeviceSettingsService::Get()->HasDmToken()) {
    LOG(WARNING) << "False recovery flag.";
    local_state.ClearPref(ash::prefs::kEnrollmentRecoveryRequired);
    return std::nullopt;
  }

  LOG(WARNING) << "Enrollment recovery required according to pref.";
  const auto serial_number = statistics_provider->GetMachineID();
  if (!serial_number || serial_number->empty()) {
    LOG(WARNING) << "Postponing recovery because machine id is missing.";
    return std::nullopt;
  }

  EnrollmentConfig recovery_config;
  recovery_config.management_domain = install_attributes.GetDomain();
  recovery_config.mode = EnrollmentConfig::MODE_RECOVERY;

  return recovery_config;
}

OOBEConfigSource ConvertToOOBEConfigSource(
    const std::string* oobe_config_source) {
  if (!oobe_config_source || oobe_config_source->empty()) {
    return OOBEConfigSource::kNone;
  }
  if (*oobe_config_source == "REMOTE_DEPLOYMENT") {
    return OOBEConfigSource::kRemoteDeployment;
  }
  if (*oobe_config_source == "PACKAGING_TOOL") {
    return OOBEConfigSource::kPackagingTool;
  }
  return OOBEConfigSource::kUnknown;
}

EnrollmentConfig::Mode GetManualFallbackMode(
    EnrollmentConfig::Mode attestation_mode) {
  switch (attestation_mode) {
    case EnrollmentConfig::MODE_ATTESTATION_INITIAL_SERVER_FORCED:
      return EnrollmentConfig::MODE_ATTESTATION_INITIAL_MANUAL_FALLBACK;
    case EnrollmentConfig::MODE_ATTESTATION_SERVER_FORCED:
      return EnrollmentConfig::MODE_ATTESTATION_MANUAL_FALLBACK;
    case EnrollmentConfig::MODE_ATTESTATION_ROLLBACK_FORCED:
      return EnrollmentConfig::MODE_ATTESTATION_ROLLBACK_MANUAL_FALLBACK;
    case EnrollmentConfig::MODE_ENROLLMENT_TOKEN_INITIAL_SERVER_FORCED:
      return EnrollmentConfig::MODE_ENROLLMENT_TOKEN_INITIAL_MANUAL_FALLBACK;
    case EnrollmentConfig::MODE_REMOTE_DEPLOYMENT_SERVER_FORCED:
      return EnrollmentConfig::MODE_REMOTE_DEPLOYMENT_MANUAL_FALLBACK;
    case EnrollmentConfig::MODE_NONE:
    case EnrollmentConfig::MODE_MANUAL:
    case EnrollmentConfig::MODE_MANUAL_REENROLLMENT:
    case EnrollmentConfig::MODE_LOCAL_FORCED:
    case EnrollmentConfig::MODE_LOCAL_ADVERTISED:
    case EnrollmentConfig::MODE_SERVER_FORCED:
    case EnrollmentConfig::MODE_SERVER_ADVERTISED:
    case EnrollmentConfig::MODE_RECOVERY:
    case EnrollmentConfig::MODE_ATTESTATION:
    case EnrollmentConfig::MODE_ATTESTATION_LOCAL_FORCED:
    case EnrollmentConfig::MODE_ATTESTATION_MANUAL_FALLBACK:
    case EnrollmentConfig::MODE_INITIAL_SERVER_FORCED:
    case EnrollmentConfig::MODE_ATTESTATION_INITIAL_MANUAL_FALLBACK:
    case EnrollmentConfig::MODE_ATTESTATION_ROLLBACK_MANUAL_FALLBACK:
    case EnrollmentConfig::MODE_ENROLLMENT_TOKEN_INITIAL_MANUAL_FALLBACK:
    case EnrollmentConfig::MODE_REMOTE_DEPLOYMENT_MANUAL_FALLBACK:
      NOTREACHED() << "Mode does not have manual fallback: "
                   << attestation_mode;
  }
}

}  // namespace

struct EnrollmentConfig::PrescribedConfig {
  EnrollmentConfig::Mode mode = MODE_NONE;
  std::string management_domain;
  std::string enrollment_token;
  OOBEConfigSource oobe_config_source = OOBEConfigSource::kNone;

  static PrescribedConfig GetPrescribedConfig(
      PrefService& local_state,
      ash::system::StatisticsProvider* statistics_provider,
      const base::DictValue& device_state,
      const ash::OobeConfiguration* oobe_configuration);
};

// static
EnrollmentConfig::PrescribedConfig
EnrollmentConfig::PrescribedConfig::GetPrescribedConfig(
    PrefService& local_state,
    ash::system::StatisticsProvider* statistics_provider,
    const base::DictValue& device_state,
    const ash::OobeConfiguration* oobe_configuration) {
  // Decide enrollment mode. Give precedence to forced variants.
  if (IsEnrollingAfterRollback()) {
    return {.mode = EnrollmentConfig::MODE_ATTESTATION_ROLLBACK_FORCED};
  }

  const std::string device_state_mode =
      GetString(device_state, kDeviceStateMode);
  const std::string device_state_management_domain =
      GetString(device_state, kDeviceStateManagementDomain);

  if (device_state_mode == kDeviceStateRestoreModeReEnrollmentEnforced) {
    return {.mode = EnrollmentConfig::MODE_SERVER_FORCED,
            .management_domain = device_state_management_domain};
  }

  if (device_state_mode == kDeviceStateInitialModeEnrollmentEnforced) {
    return {.mode = EnrollmentConfig::MODE_INITIAL_SERVER_FORCED,
            .management_domain = device_state_management_domain};
  }

  if (device_state_mode == kDeviceStateRestoreModeReEnrollmentZeroTouch) {
    return {.mode = EnrollmentConfig::MODE_ATTESTATION_SERVER_FORCED,
            .management_domain = device_state_management_domain};
  }

  if (device_state_mode == kDeviceStateInitialModeEnrollmentZeroTouch) {
    return {.mode = EnrollmentConfig::MODE_ATTESTATION_INITIAL_SERVER_FORCED,
            .management_domain = device_state_management_domain};
  }

  if (device_state_mode == kDeviceStateInitialModeTokenEnrollment) {
    std::optional<std::string> enrollment_token =
        GetEnrollmentToken(oobe_configuration);
    // TODO(b/329271128): CHECK to ensure enrollment_token always has value
    // after this bug is fixed.
    if (enrollment_token.has_value()) {
      OOBEConfigSource oobe_config_source = ConvertToOOBEConfigSource(
          oobe_configuration->configuration().FindString(
              ash::configuration::kSource));
      EnrollmentConfig::Mode mode;
      switch (oobe_config_source) {
        case policy::OOBEConfigSource::kRemoteDeployment:
          mode = MODE_REMOTE_DEPLOYMENT_SERVER_FORCED;
          break;
        case policy::OOBEConfigSource::kPackagingTool:
        case policy::OOBEConfigSource::kUnknown:
        case policy::OOBEConfigSource::kNone:
          mode = MODE_ENROLLMENT_TOKEN_INITIAL_SERVER_FORCED;
          break;
      }
      return {.mode = mode,
              .enrollment_token = std::move(enrollment_token.value()),
              .oobe_config_source = oobe_config_source};
    } else {
      return {.mode = EnrollmentConfig::MODE_NONE};
    }
  }

  const bool pref_enrollment_auto_start_present =
      local_state.HasPrefPath(ash::prefs::kDeviceEnrollmentAutoStart);
  const bool pref_enrollment_auto_start =
      local_state.GetBoolean(ash::prefs::kDeviceEnrollmentAutoStart);

  const bool pref_enrollment_can_exit_present =
      local_state.HasPrefPath(ash::prefs::kDeviceEnrollmentCanExit);
  const bool pref_enrollment_can_exit =
      local_state.GetBoolean(ash::prefs::kDeviceEnrollmentCanExit);

  if (pref_enrollment_auto_start_present && pref_enrollment_auto_start &&
      pref_enrollment_can_exit_present && !pref_enrollment_can_exit) {
    return {.mode = EnrollmentConfig::MODE_LOCAL_FORCED};
  }

  const bool oem_is_managed = ash::system::StatisticsProvider::FlagValueToBool(
      statistics_provider->GetMachineFlag(
          ash::system::kOemIsEnterpriseManagedKey),
      /*default_value=*/false);
  const bool oem_can_exit_enrollment =
      ash::system::StatisticsProvider::FlagValueToBool(
          statistics_provider->GetMachineFlag(
              ash::system::kOemCanExitEnterpriseEnrollmentKey),
          /*default_value=*/true);

  if (oem_is_managed && !oem_can_exit_enrollment) {
    return {.mode = EnrollmentConfig::MODE_LOCAL_FORCED};
  }

  if (local_state.GetBoolean(ash::prefs::kOobeComplete)) {
    // If OOBE is complete, don't return advertised modes as there's currently
    // no way to make sure advertised enrollment only gets shown once.
    return {.mode = EnrollmentConfig::MODE_NONE};
  }

  if (device_state_mode == kDeviceStateRestoreModeReEnrollmentRequested) {
    return {.mode = EnrollmentConfig::MODE_SERVER_ADVERTISED,
            .management_domain = device_state_management_domain};
  }

  if (pref_enrollment_auto_start_present && pref_enrollment_auto_start) {
    return {.mode = EnrollmentConfig::MODE_LOCAL_ADVERTISED};
  }

  if (oem_is_managed) {
    return {.mode = EnrollmentConfig::MODE_LOCAL_ADVERTISED};
  }

  return {.mode = EnrollmentConfig::MODE_NONE};
}

struct EnrollmentConfig::PrescribedLicense {
  bool is_license_packaged_with_device = false;
  EnrollmentConfig::AssignedUpgradeType assigned_upgrade_type =
      AssignedUpgradeType::kAssignedUpgradeTypeUnspecified;
  LicenseType license_type = LicenseType::kNone;

  static PrescribedLicense GetPrescribedLicense(
      const base::DictValue& device_state);
};

// static
EnrollmentConfig::PrescribedLicense
EnrollmentConfig::PrescribedLicense::GetPrescribedLicense(
    const base::DictValue& device_state) {
  EnrollmentConfig::AssignedUpgradeType assigned_upgrade_type =
      EnrollmentConfig::AssignedUpgradeType::
          kAssignedUpgradeTypeChromeEnterprise;

  const std::string assigned_upgrade_type_str =
      GetString(device_state, kDeviceStateAssignedUpgradeType);

  if (assigned_upgrade_type_str ==
      kDeviceStateAssignedUpgradeTypeChromeEnterprise) {
    assigned_upgrade_type = EnrollmentConfig::AssignedUpgradeType::
        kAssignedUpgradeTypeChromeEnterprise;
  } else if (assigned_upgrade_type_str ==
             kDeviceStateAssignedUpgradeTypeKiosk) {
    assigned_upgrade_type = EnrollmentConfig::AssignedUpgradeType::
        kAssignedUpgradeTypeKioskAndSignage;
  }

  const bool is_license_packaged_with_device =
      device_state.FindBool(kDeviceStatePackagedLicense).value_or(false);
  const std::string license_type_str =
      GetString(device_state, kDeviceStateLicenseType);

  const LicenseType license_type =
      GetLicenseTypeToUse(license_type_str, is_license_packaged_with_device,
                          assigned_upgrade_type_str);

  return {.is_license_packaged_with_device = is_license_packaged_with_device,
          .assigned_upgrade_type = assigned_upgrade_type,
          .license_type = license_type};
}

EnrollmentConfig::EnrollmentConfig() = default;
EnrollmentConfig::EnrollmentConfig(const EnrollmentConfig& other) = default;
EnrollmentConfig::~EnrollmentConfig() = default;

EnrollmentConfig::EnrollmentConfig(PrescribedConfig prescribed_config,
                                   PrescribedLicense prescribed_license)
    : mode(prescribed_config.mode),
      management_domain(std::move(prescribed_config.management_domain)),
      is_license_packaged_with_device(
          prescribed_license.is_license_packaged_with_device),
      license_type(prescribed_license.license_type),
      assigned_upgrade_type(prescribed_license.assigned_upgrade_type),
      enrollment_token(std::move(prescribed_config.enrollment_token)),
      oobe_config_source(prescribed_config.oobe_config_source) {}

// static
EnrollmentConfig EnrollmentConfig::GetPrescribedEnrollmentConfig(
    PrefService& local_state) {
  return GetPrescribedEnrollmentConfig(
      local_state, *ash::InstallAttributes::Get(),
      ash::system::StatisticsProvider::GetInstance(),
      ash::OobeConfiguration::Get());
}

// static
EnrollmentConfig EnrollmentConfig::GetPrescribedEnrollmentConfig(
    PrefService& local_state,
    const ash::InstallAttributes& install_attributes,
    ash::system::StatisticsProvider* statistics_provider,
    const ash::OobeConfiguration* oobe_configuration) {
  DCHECK(statistics_provider);
  DCHECK(oobe_configuration);

  // Return enrollment recovery config if required.
  if (std::optional<EnrollmentConfig> maybe_recovery_config =
          GetPrescribedRecoveryConfig(local_state, install_attributes,
                                      statistics_provider)) {
    return maybe_recovery_config.value();
  }

  // If OOBE is done and the device is enrolled, no need to enroll.
  if (local_state.GetBoolean(ash::prefs::kOobeComplete) &&
      install_attributes.IsCloudManaged()) {
    return EnrollmentConfig{};
  }

  const base::DictValue& device_state =
      local_state.GetDict(ash::prefs::kServerBackedDeviceState);

  return EnrollmentConfig(
      PrescribedConfig::GetPrescribedConfig(local_state, statistics_provider,
                                            device_state, oobe_configuration),
      PrescribedLicense::GetPrescribedLicense(device_state));
}

// static
EnrollmentConfig EnrollmentConfig::GetDemoModeEnrollmentConfig() {
  EnrollmentConfig config;
  config.mode = EnrollmentConfig::MODE_ATTESTATION;
  config.management_domain = kDemoModeDomain;
  config.license_type = LicenseType::kEnterprise;
  return config;
}

EnrollmentConfig EnrollmentConfig::GetEffectiveConfig() const {
  if (should_enroll()) {
    return *this;
  }

  EnrollmentConfig manual_config = *this;
  // TODO(b/332529631): It is not possible to have `managed_domain` and
  // `MODE_NONE` at the same time. Figure out if `MODE_MANUAL_REENROLLMENT`
  // needs to be deprecated.
  manual_config.mode =
      management_domain.empty() ? MODE_MANUAL : MODE_MANUAL_REENROLLMENT;
  return manual_config;
}

std::optional<EnrollmentConfig> EnrollmentConfig::GetManualFallbackConfig()
    const {
  if (!is_mode_with_manual_fallback()) {
    return std::nullopt;
  }

  std::optional<EnrollmentConfig> manual_fallback_config = *this;
  manual_fallback_config->mode = GetManualFallbackMode(mode);

  return manual_fallback_config;
}

std::ostream& operator<<(std::ostream& os, const EnrollmentConfig::Mode& mode) {
  return os << ToStringView(mode);
}

std::ostream& operator<<(std::ostream& os, const EnrollmentConfig& config) {
  return os << "EnrollmentConfig(" << config.mode << ")";
}

}  // namespace policy
