// 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/settings/about_flags.h"

#include <string_view>

#include "ash/constants/chrome_pref_names.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/values.h"
#include "chrome/browser/about_flags.h"
#include "chrome/browser/ash/login/session/user_session_manager.h"
#include "chrome/browser/site_isolation/about_flags.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/dbus/session_manager/session_manager_client.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/account_id/account_id.h"
#include "components/ownership/owner_settings_service.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user_manager.h"
#include "components/webui/flags/flags_storage.h"
#include "components/webui/flags/flags_ui_pref_names.h"
#include "third_party/cros_system_api/switches/chrome_switches.h"

namespace ash {
namespace about_flags {

namespace {

std::set<std::string> ParseFlagsFromCommandLine(
    base::CommandLine* command_line) {
  std::set<std::string> flags;
  std::string encoded =
      command_line->GetSwitchValueNative(chromeos::switches::kFeatureFlags);
  if (encoded.empty()) {
    return flags;
  }

  auto flags_list =
      base::JSONReader::Read(encoded, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!flags_list) {
    LOG(WARNING) << "Failed to parse feature flags configuration";
    return flags;
  }

  for (const auto& flag : flags_list.value().GetList()) {
    if (!flag.is_string()) {
      LOG(WARNING) << "Invalid entry in encoded feature flags";
      continue;
    }
    flags.insert(flag.GetString());
  }
  return flags;
}

std::map<std::string, std::string> ParseOriginListFlagsFromCommmandLine(
    base::CommandLine* command_line) {
  std::map<std::string, std::string> origin_list_flags;
  std::string encoded = command_line->GetSwitchValueNative(
      chromeos::switches::kFeatureFlagsOriginList);
  if (encoded.empty()) {
    return origin_list_flags;
  }

  auto origin_list_flags_dict =
      base::JSONReader::Read(encoded, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!origin_list_flags_dict) {
    LOG(WARNING) << "Failed to parse origin list configuration";
    return origin_list_flags;
  }

  for (const auto entry : origin_list_flags_dict->GetDict()) {
    if (!entry.second.is_string()) {
      LOG(WARNING) << "Invalid entry in encoded origin list flags";
      continue;
    }
    origin_list_flags[entry.first] = entry.second.GetString();
  }
  return origin_list_flags;
}

}  // namespace

OwnerFlagsStorage::OwnerFlagsStorage(
    PrefService* prefs,
    ownership::OwnerSettingsService* owner_settings_service)
    : flags_ui::PrefServiceFlagsStorage(prefs),
      owner_settings_service_(owner_settings_service) {}

OwnerFlagsStorage::~OwnerFlagsStorage() = default;

bool OwnerFlagsStorage::SetFlags(const std::set<std::string>& flags) {
  // Write the flags configuration to profile preferences, which are used to
  // determine flags to apply when launching a user session.
  PrefServiceFlagsStorage::SetFlags(flags);

  // Also write the flags to device settings so they get applied to the Chrome
  // OS login screen. The device setting is read by session_manager and passed
  // to Chrome via a command line flag on startup.
  base::ListValue feature_flags_list;
  for (const auto& flag : flags) {
    feature_flags_list.Append(flag);
  }
  owner_settings_service_->Set(kFeatureFlags,
                               base::Value(std::move(feature_flags_list)));

  return true;
}

ReadOnlyFlagsStorage::ReadOnlyFlagsStorage(
    const std::set<std::string>& flags,
    const std::map<std::string, std::string>& origin_list_flags)
    : flags_(flags), origin_list_flags_(origin_list_flags) {}

ReadOnlyFlagsStorage::ReadOnlyFlagsStorage(base::CommandLine* command_line)
    : flags_(ParseFlagsFromCommandLine(command_line)),
      origin_list_flags_(ParseOriginListFlagsFromCommmandLine(command_line)) {}

ReadOnlyFlagsStorage::~ReadOnlyFlagsStorage() = default;

std::set<std::string> ReadOnlyFlagsStorage::GetFlags() const {
  return flags_;
}

bool ReadOnlyFlagsStorage::SetFlags(const std::set<std::string>& flags) {
  return false;
}

void ReadOnlyFlagsStorage::CommitPendingWrites() {}

std::string ReadOnlyFlagsStorage::GetOriginListFlag(
    const std::string& internal_entry_name) const {
  const auto& entry = origin_list_flags_.find(internal_entry_name);
  return entry != origin_list_flags_.end() ? entry->second : std::string();
}

void ReadOnlyFlagsStorage::SetOriginListFlag(
    const std::string& internal_entry_name,
    const std::string& origin_list_value) {}

std::string ReadOnlyFlagsStorage::GetStringFlag(
    const std::string& internal_entry_name) const {
  return GetOriginListFlag(internal_entry_name);
}

void ReadOnlyFlagsStorage::SetStringFlag(const std::string& internal_entry_name,
                                         const std::string& string_value) {}

base::DictValue ReadOnlyFlagsStorage::GetCustomizedFlags() const {
  base::DictValue customized_flags;
  for (const auto& [name, value] : origin_list_flags_) {
    customized_flags.Set(name, value);
  }
  return customized_flags;
}

void ReadOnlyFlagsStorage::SetCustomizedFlags(
    const base::DictValue& customized_flags) {}

FeatureFlagsUpdate::FeatureFlagsUpdate(
    const ::flags_ui::FlagsStorage& flags_storage,
    PrefService* profile_prefs) {
  flags_ = flags_storage.GetFlags();
  ApplyUserPolicyToFlags(profile_prefs, &flags_);

  for (const auto& flag : flags_) {
    const auto origin_list_flag = flags_storage.GetOriginListFlag(flag);
    if (!origin_list_flag.empty()) {
      origin_list_flags_[flag] = origin_list_flag;
    }
  }
}

FeatureFlagsUpdate::~FeatureFlagsUpdate() = default;

bool FeatureFlagsUpdate::DiffersFromCommandLine(
    base::CommandLine* cmdline,
    std::set<std::string>* flags_difference) {
  flags_difference->clear();

  const auto cmdline_flags = ParseFlagsFromCommandLine(cmdline);
  std::set_symmetric_difference(
      flags_.begin(), flags_.end(), cmdline_flags.begin(), cmdline_flags.end(),
      std::inserter(*flags_difference, flags_difference->begin()));

  auto lookup = [](const std::map<std::string, std::string>& origin_list_flags,
                   const std::string& key) {
    const auto entry = origin_list_flags.find(key);
    return entry == origin_list_flags.end() ? std::nullopt
                                            : std::make_optional(entry->second);
  };
  const auto cmdline_origin_list_flags =
      ParseOriginListFlagsFromCommmandLine(cmdline);
  for (const auto& flag : flags_) {
    if (lookup(origin_list_flags_, flag) !=
        lookup(cmdline_origin_list_flags, flag)) {
      flags_difference->insert(flag);
    }
  }

  return !flags_difference->empty();
}

void FeatureFlagsUpdate::UpdateSessionManager() {
  // Early out so that switches for secondary users are not applied to the whole
  // session.
  // Note that this early return could be removed when things like flags UI of
  // secondary users are fixed properly - in that case a CHECK should be added
  // that the active user is the primary user.
  user_manager::UserManager* user_manager = user_manager::UserManager::Get();
  const user_manager::User* primary_user = user_manager->GetPrimaryUser();
  if (!primary_user || primary_user != user_manager->GetActiveUser())
    return;

  auto account_id = cryptohome::CreateAccountIdentifierFromAccountId(
      primary_user->GetAccountId());
  SessionManagerClient::Get()->SetFeatureFlagsForUser(
      account_id, {flags_.begin(), flags_.end()}, origin_list_flags_);
}

// static
void FeatureFlagsUpdate::ApplyUserPolicyToFlags(PrefService* user_profile_prefs,
                                                std::set<std::string>* flags) {
  // Get target value for --site-per-process for the user session according to
  // policy. If it is supposed to be enabled, make sure it can not be disabled
  // using flags-induced command-line switches.
  const PrefService::Preference* site_per_process_pref =
      user_profile_prefs->FindPreference(ash::chrome_prefs::kSitePerProcess);
  if (site_per_process_pref->IsManaged() &&
      site_per_process_pref->GetValue()->GetBool()) {
    flags->erase(::about_flags::SiteIsolationTrialOptOutChoiceEnabled());
  }
}

}  // namespace about_flags
}  // namespace ash
