// Copyright 2013 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/core/device_local_account.h"

#include <stddef.h>

#include <memory>
#include <optional>
#include <utility>

#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chrome/browser/ash/ownership/owner_settings_service_ash.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user_names.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"

namespace policy {

namespace {

bool GetString(const base::DictValue& dict,
               const char* key,
               std::string* result) {
  const std::string* value = dict.FindString(key);
  if (!value) {
    return false;
  }
  *result = *value;
  return true;
}

bool IsKioskType(DeviceLocalAccountType type) {
  switch (type) {
    case DeviceLocalAccountType::kKioskApp:
    case DeviceLocalAccountType::kWebKioskApp:
    case DeviceLocalAccountType::kKioskIsolatedWebApp:
    case DeviceLocalAccountType::kArcvmKioskApp:
      return true;
    case DeviceLocalAccountType::kPublicSession:
    case DeviceLocalAccountType::kSamlPublicSession:
      return false;
  }
  NOTREACHED();
}

}  // namespace

WebKioskAppBasicInfo::WebKioskAppBasicInfo(const std::string& url,
                                           const std::string& title,
                                           const std::string& icon_url)
    : url_(url), title_(title), icon_url_(icon_url) {}

WebKioskAppBasicInfo::WebKioskAppBasicInfo() = default;

WebKioskAppBasicInfo::~WebKioskAppBasicInfo() = default;

IsolatedWebAppKioskBasicInfo::IsolatedWebAppKioskBasicInfo(
    std::string web_bundle_id,
    std::string update_manifest_url,
    std::string update_channel,
    std::string pinned_version,
    bool allow_downgrades)
    : web_bundle_id_(std::move(web_bundle_id)),
      update_manifest_url_(std::move(update_manifest_url)),
      update_channel_(std::move(update_channel)),
      pinned_version_(std::move(pinned_version)),
      allow_downgrades_(allow_downgrades) {}

IsolatedWebAppKioskBasicInfo::IsolatedWebAppKioskBasicInfo() = default;

IsolatedWebAppKioskBasicInfo::~IsolatedWebAppKioskBasicInfo() = default;

IsolatedWebAppKioskBasicInfo::IsolatedWebAppKioskBasicInfo(
    const IsolatedWebAppKioskBasicInfo& other) = default;

IsolatedWebAppKioskBasicInfo& IsolatedWebAppKioskBasicInfo::operator=(
    const IsolatedWebAppKioskBasicInfo&) = default;

ArcvmKioskAppBasicInfo::ArcvmKioskAppBasicInfo(const std::string& package_name,
                                               const std::string& class_name,
                                               const std::string& action,
                                               const std::string& display_name)
    : package_name_(package_name),
      class_name_(class_name),
      action_(action),
      display_name_(display_name) {}

ArcvmKioskAppBasicInfo::ArcvmKioskAppBasicInfo(
    const ArcvmKioskAppBasicInfo& other) = default;

ArcvmKioskAppBasicInfo& ArcvmKioskAppBasicInfo::operator=(
    const ArcvmKioskAppBasicInfo&) = default;

ArcvmKioskAppBasicInfo::ArcvmKioskAppBasicInfo() = default;

ArcvmKioskAppBasicInfo::~ArcvmKioskAppBasicInfo() = default;

DeviceLocalAccount::DeviceLocalAccount(DeviceLocalAccountType type,
                                       EphemeralMode ephemeral_mode,
                                       const std::string& account_id,
                                       const std::string& kiosk_app_id,
                                       const std::string& kiosk_app_update_url)
    : type(type),
      ephemeral_mode(ephemeral_mode),
      account_id(account_id),
      user_id(GenerateDeviceLocalAccountUserId(account_id, type)),
      kiosk_app_id(kiosk_app_id),
      kiosk_app_update_url(kiosk_app_update_url) {}

DeviceLocalAccount::DeviceLocalAccount(
    EphemeralMode ephemeral_mode,
    const WebKioskAppBasicInfo& web_kiosk_app_info,
    const std::string& account_id)
    : type(DeviceLocalAccountType::kWebKioskApp),
      ephemeral_mode(ephemeral_mode),
      account_id(account_id),
      user_id(GenerateDeviceLocalAccountUserId(account_id, type)),
      web_kiosk_app_info(web_kiosk_app_info) {}

DeviceLocalAccount::DeviceLocalAccount(
    EphemeralMode ephemeral_mode,
    const IsolatedWebAppKioskBasicInfo& kiosk_iwa_info,
    const std::string& account_id)
    : type(DeviceLocalAccountType::kKioskIsolatedWebApp),
      ephemeral_mode(ephemeral_mode),
      account_id(account_id),
      user_id(GenerateDeviceLocalAccountUserId(account_id, type)),
      kiosk_iwa_info(kiosk_iwa_info) {}

DeviceLocalAccount::DeviceLocalAccount(
    EphemeralMode ephemeral_mode,
    const ArcvmKioskAppBasicInfo& arcvm_kiosk_app_info,
    const std::string& account_id)
    : type(DeviceLocalAccountType::kArcvmKioskApp),
      ephemeral_mode(ephemeral_mode),
      account_id(account_id),
      user_id(GenerateDeviceLocalAccountUserId(account_id, type)),
      arcvm_kiosk_app_info(arcvm_kiosk_app_info) {}

DeviceLocalAccount::DeviceLocalAccount(const DeviceLocalAccount& other) =
    default;

DeviceLocalAccount::~DeviceLocalAccount() = default;

std::vector<DeviceLocalAccount> GetDeviceLocalAccounts(
    ash::CrosSettings* cros_settings) {
  // TODO(crbug.com/40636049): handle TYPE_SAML_PUBLIC_SESSION
  std::vector<DeviceLocalAccount> accounts;

  const base::ListValue* list = nullptr;
  if (!cros_settings->GetList(ash::kAccountsPrefDeviceLocalAccounts, &list)) {
    return accounts;
  }

  absl::flat_hash_set<std::string> account_ids;
  for (size_t i = 0; i < list->size(); ++i) {
    const base::Value& entry = (*list)[i];
    if (!entry.is_dict()) {
      LOG(ERROR) << "Corrupt entry in device-local account list at index " << i
                 << ".";
      continue;
    }

    const base::DictValue& entry_dict = entry.GetDict();
    std::string account_id;
    if (!GetString(entry_dict, ash::kAccountsPrefDeviceLocalAccountsKeyId,
                   &account_id) ||
        account_id.empty()) {
      LOG(ERROR) << "Missing account ID in device-local account list at index "
                 << i << ".";
      continue;
    }

    std::optional<int> raw_type =
        entry_dict.FindInt(ash::kAccountsPrefDeviceLocalAccountsKeyType);
    if (!raw_type || !IsValidDeviceLocalAccountType(*raw_type)) {
      LOG(ERROR) << "Missing or invalid account type in device-local account "
                 << "list at index " << i << ".";
      continue;
    }
    auto type = static_cast<DeviceLocalAccountType>(*raw_type);

    DeviceLocalAccount::EphemeralMode ephemeral_mode_value =
        DeviceLocalAccount::EphemeralMode::kUnset;
    if (IsKioskType(type)) {
      std::optional<int> ephemeral_mode = entry_dict.FindInt(
          ash::kAccountsPrefDeviceLocalAccountsKeyEphemeralMode);
      if (!ephemeral_mode || ephemeral_mode.value() < 0 ||
          ephemeral_mode.value() >
              static_cast<int>(DeviceLocalAccount::EphemeralMode::kMaxValue)) {
        LOG(ERROR) << "Missing or invalid ephemeral mode (value="
                   << ephemeral_mode.value_or(-1)
                   << ") in device-local account list at index " << i
                   << ", using default kUnset value for ephemeral mode.";
      } else {
        ephemeral_mode_value = static_cast<DeviceLocalAccount::EphemeralMode>(
            ephemeral_mode.value());
      }
    }

    if (!account_ids.insert(account_id).second) {
      LOG(ERROR) << "Duplicate entry in device-local account list at index "
                 << i << ": " << account_id << ".";
      continue;
    }

    switch (type) {
      case DeviceLocalAccountType::kPublicSession:
        accounts.emplace_back(DeviceLocalAccountType::kPublicSession,
                              ephemeral_mode_value, account_id, "", "");
        break;
      case DeviceLocalAccountType::kSamlPublicSession:
        accounts.emplace_back(DeviceLocalAccountType::kSamlPublicSession,
                              ephemeral_mode_value, account_id, "", "");
        break;
      case DeviceLocalAccountType::kKioskApp: {
        std::string kiosk_app_id;
        std::string kiosk_app_update_url;
        if (!GetString(entry_dict,
                       ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppId,
                       &kiosk_app_id)) {
          LOG(ERROR) << "Missing app ID in device-local account entry at index "
                     << i << ".";
          continue;
        }
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppUpdateURL,
                  &kiosk_app_update_url);

        accounts.emplace_back(DeviceLocalAccountType::kKioskApp,
                              ephemeral_mode_value, account_id, kiosk_app_id,
                              kiosk_app_update_url);
        break;
      }
      case DeviceLocalAccountType::kWebKioskApp: {
        std::string url;
        std::string title;
        std::string icon_url;
        if (!GetString(entry_dict,
                       ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskUrl,
                       &url)) {
          LOG(ERROR) << "Missing install url in Web kiosk type device-local "
                        "account at index "
                     << i << ".";
          continue;
        }

        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskTitle,
                  &title);
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskIconUrl,
                  &icon_url);
        accounts.emplace_back(ephemeral_mode_value,
                              WebKioskAppBasicInfo(url, title, icon_url),
                              account_id);
        break;
      }
      case DeviceLocalAccountType::kKioskIsolatedWebApp: {
        std::string web_bundle_id;
        if (!GetString(entry_dict,
                       ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskBundleId,
                       &web_bundle_id)) {
          LOG(ERROR) << "Missing web bundle ID in IWA kiosk type device-local "
                        "account at index "
                     << i << ".";
          continue;
        }

        std::string update_manifest_url;
        if (!GetString(
                entry_dict,
                ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskUpdateUrl,
                &update_manifest_url)) {
          LOG(ERROR) << "Missing manifest url in IWA kiosk type device-local "
                        "account at index "
                     << i << ".";
          continue;
        }

        std::string update_channel;
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskUpdateChannel,
                  &update_channel);

        std::string pinned_version;
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskPinnedVersion,
                  &pinned_version);

        bool allow_downgrades =
            entry_dict
                .FindBool(
                    ash::
                        kAccountsPrefDeviceLocalAccountsKeyIwaKioskAllowDowngrades)
                .value_or(false);

        accounts.emplace_back(
            ephemeral_mode_value,
            IsolatedWebAppKioskBasicInfo(web_bundle_id, update_manifest_url,
                                         update_channel, pinned_version,
                                         allow_downgrades),
            account_id);
        break;
      }
      case DeviceLocalAccountType::kArcvmKioskApp: {
        std::string package_name;
        std::string class_name;
        std::string action;
        std::string display_name;
        if (!GetString(
                entry_dict,
                ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskPackage,
                &package_name)) {
          LOG(ERROR) << "Missing package name in ARC kiosk type device-local "
                        "account at index "
                     << i << ".";
          continue;
        }
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskClass,
                  &class_name);
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskAction,
                  &action);
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskDisplayName,
                  &display_name);
        const ArcvmKioskAppBasicInfo arcvm_kiosk_app(package_name, class_name,
                                                     action, display_name);

        accounts.emplace_back(ephemeral_mode_value, arcvm_kiosk_app,
                              account_id);
        break;
      }
    }
  }

  return accounts;
}

void SetDeviceLocalAccountsForTesting(
    ash::OwnerSettingsServiceAsh* service,
    const std::vector<DeviceLocalAccount>& accounts) {
  // TODO(crbug.com/40636049): handle TYPE_SAML_PUBLIC_SESSION
  base::ListValue list;
  for (const auto& account : accounts) {
    auto entry =
        base::DictValue()
            .Set(ash::kAccountsPrefDeviceLocalAccountsKeyId, account.account_id)
            .Set(ash::kAccountsPrefDeviceLocalAccountsKeyType,
                 static_cast<int>(account.type))
            .Set(ash::kAccountsPrefDeviceLocalAccountsKeyEphemeralMode,
                 static_cast<int>(account.ephemeral_mode));
    switch (account.type) {
      case DeviceLocalAccountType::kPublicSession:
      case DeviceLocalAccountType::kSamlPublicSession:
        // Do nothing.
        break;
      case DeviceLocalAccountType::kKioskApp:
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppId,
                  account.kiosk_app_id);
        if (!account.kiosk_app_update_url.empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppUpdateURL,
                    account.kiosk_app_update_url);
        }
        break;
      case DeviceLocalAccountType::kWebKioskApp:
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskUrl,
                  account.web_kiosk_app_info.url());
        if (!account.web_kiosk_app_info.title().empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskTitle,
                    account.web_kiosk_app_info.title());
        }
        if (!account.web_kiosk_app_info.icon_url().empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskIconUrl,
                    account.web_kiosk_app_info.icon_url());
        }
        break;
      case DeviceLocalAccountType::kKioskIsolatedWebApp:
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskBundleId,
                  account.kiosk_iwa_info.web_bundle_id());
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskUpdateUrl,
                  account.kiosk_iwa_info.update_manifest_url());
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskUpdateChannel,
                  account.kiosk_iwa_info.update_channel());
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskPinnedVersion,
                  account.kiosk_iwa_info.pinned_version());
        entry.Set(
            ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskAllowDowngrades,
            account.kiosk_iwa_info.allow_downgrades());
        break;
      case DeviceLocalAccountType::kArcvmKioskApp:
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskPackage,
                  account.arcvm_kiosk_app_info.package_name());
        if (!account.arcvm_kiosk_app_info.class_name().empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskClass,
                    account.arcvm_kiosk_app_info.class_name());
        }
        if (!account.arcvm_kiosk_app_info.action().empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskAction,
                    account.arcvm_kiosk_app_info.action());
        }
        if (!account.arcvm_kiosk_app_info.display_name().empty()) {
          entry.Set(
              ash::kAccountsPrefDeviceLocalAccountsKeyArcvmKioskDisplayName,
              account.arcvm_kiosk_app_info.display_name());
        }
        break;
    }
    list.Append(std::move(entry));
  }

  service->Set(ash::kAccountsPrefDeviceLocalAccounts,
               base::Value(std::move(list)));
}

}  // namespace policy
