// Copyright 2019 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/flags/flags_ui_handler.h"

#include "base/functional/bind.h"
#include "build/build_config.h"
#include "chrome/browser/about_flags.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/common/channel_info.h"
#include "components/version_info/channel.h"
#include "components/webui/flags/flags_storage.h"
#include "components/webui/flags/flags_ui_constants.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/settings/about_flags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#endif

namespace {
bool ExtractKeyValue(const base::ListValue& args,
                     std::string& key,
                     std::string& value) {
  if (args.size() != 2) {
    return false;
  }

  if (!args[0].is_string() || !args[1].is_string()) {
    return false;
  }
  key = args[0].GetString();
  value = args[1].GetString();
  return !key.empty();
}
}  // namespace

FlagsUIHandler::FlagsUIHandler()
    : access_(flags_ui::kGeneralAccessFlagsOnly),
      experimental_features_callback_id_(""),
      deprecated_features_callback_id_("") {}

FlagsUIHandler::~FlagsUIHandler() = default;

void FlagsUIHandler::RegisterMessages() {
  web_ui()->RegisterMessageCallback(
      "requestDeprecatedFeatures",
      base::BindRepeating(&FlagsUIHandler::HandleRequestDeprecatedFeatures,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      flags_ui::kRequestExperimentalFeatures,
      base::BindRepeating(&FlagsUIHandler::HandleRequestExperimentalFeatures,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      flags_ui::kEnableExperimentalFeature,
      base::BindRepeating(
          &FlagsUIHandler::HandleEnableExperimentalFeatureMessage,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      flags_ui::kSetOriginListFlag,
      base::BindRepeating(&FlagsUIHandler::HandleSetOriginListFlagMessage,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      flags_ui::kSetStringFlag,
      base::BindRepeating(&FlagsUIHandler::HandleSetStringFlagMessage,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      flags_ui::kRestartBrowser,
      base::BindRepeating(&FlagsUIHandler::HandleRestartBrowser,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      flags_ui::kResetAllFlags,
      base::BindRepeating(&FlagsUIHandler::HandleResetAllFlags,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "exportFlags", base::BindRepeating(&FlagsUIHandler::HandleExportFlags,
                                         base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "importFlags", base::BindRepeating(&FlagsUIHandler::HandleImportFlags,
                                         base::Unretained(this)));
}

void FlagsUIHandler::Init(std::unique_ptr<flags_ui::FlagsStorage> flags_storage,
                          flags_ui::FlagAccess access) {
  flags_storage_ = std::move(flags_storage);
  access_ = access;

  if (!experimental_features_callback_id_.empty()) {
    SendExperimentalFeatures(/*deprecated_features_only=*/false);
  }

  if (!deprecated_features_callback_id_.empty()) {
    SendExperimentalFeatures(/*deprecated_features_only=*/true);
  }
}

void FlagsUIHandler::HandleRequestDeprecatedFeatures(
    const base::ListValue& args) {
  AllowJavascript();
  const base::Value& callback_id = args[0];

  deprecated_features_callback_id_ = callback_id.GetString();
  // Bail out if the handler hasn't been initialized yet. The request will be
  // handled after the initialization.
  if (!flags_storage_) {
    return;
  }

  SendExperimentalFeatures(/*deprecated_features_only=*/true);
}

void FlagsUIHandler::HandleRequestExperimentalFeatures(
    const base::ListValue& args) {
  AllowJavascript();
  const base::Value& callback_id = args[0];

  experimental_features_callback_id_ = callback_id.GetString();
  // Bail out if the handler hasn't been initialized yet. The request will be
  // handled after the initialization.
  if (!flags_storage_) {
    return;
  }

  SendExperimentalFeatures(/*deprecated_features_only=*/false);
}

void FlagsUIHandler::SendExperimentalFeatures(bool deprecated_features_only) {
  base::DictValue results;

  base::ListValue supported_features;
  base::ListValue unsupported_features;

  if (deprecated_features_only) {
    about_flags::GetFlagFeatureEntriesForDeprecatedPage(
        flags_storage_.get(), access_, supported_features,
        unsupported_features);
  } else {
    about_flags::GetFlagFeatureEntries(flags_storage_.get(), access_,
                                       supported_features,
                                       unsupported_features);
  }

  results.Set(flags_ui::kSupportedFeatures, std::move(supported_features));
  results.Set(flags_ui::kUnsupportedFeatures, std::move(unsupported_features));
  results.Set(flags_ui::kNeedsRestart,
              about_flags::IsRestartNeededToCommitChanges());
  results.Set(flags_ui::kShowOwnerWarning,
              access_ == flags_ui::kGeneralAccessFlagsOnly);
  results.Set("importExportEnabled",
              base::FeatureList::IsEnabled(features::kImportExportFlags));

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS)
  version_info::Channel channel = chrome::GetChannel();
  results.Set(
      flags_ui::kShowBetaChannelPromotion,
      channel == version_info::Channel::STABLE && !deprecated_features_only);
  results.Set(
      flags_ui::kShowDevChannelPromotion,
      channel == version_info::Channel::BETA && !deprecated_features_only);
#else
  results.Set(flags_ui::kShowBetaChannelPromotion, false);
  results.Set(flags_ui::kShowDevChannelPromotion, false);
#endif
  if (deprecated_features_only) {
    ResolveJavascriptCallback(base::Value(deprecated_features_callback_id_),
                              results);
    deprecated_features_callback_id_.clear();
  } else {
    ResolveJavascriptCallback(base::Value(experimental_features_callback_id_),
                              results);
    experimental_features_callback_id_.clear();
  }
}

void FlagsUIHandler::HandleEnableExperimentalFeatureMessage(
    const base::ListValue& args) {
  DCHECK(flags_storage_);
  DCHECK_EQ(2u, args.size());
  if (args.size() != 2) {
    return;
  }

  if (!args[0].is_string() || !args[1].is_string()) {
    NOTREACHED();
  }
  const std::string& entry_internal_name = args[0].GetString();
  const std::string& enable_str = args[1].GetString();
  if (entry_internal_name.empty()) {
    NOTREACHED();
  }

  about_flags::SetFeatureEntryEnabled(flags_storage_.get(), entry_internal_name,
                                      enable_str == "true");
}

void FlagsUIHandler::HandleSetOriginListFlagMessage(
    const base::ListValue& args) {
  DCHECK(flags_storage_);
  std::string entry_internal_name, value_str;
  if (!ExtractKeyValue(args, entry_internal_name, value_str)) {
    NOTREACHED();
  }

  about_flags::SetOriginListFlag(entry_internal_name, value_str,
                                 flags_storage_.get());
}

void FlagsUIHandler::HandleSetStringFlagMessage(const base::ListValue& args) {
  DCHECK(flags_storage_);
  std::string entry_internal_name, value_str;
  if (!ExtractKeyValue(args, entry_internal_name, value_str)) {
    NOTREACHED();
  }

  about_flags::SetStringFlag(entry_internal_name, value_str,
                             flags_storage_.get());
}

void FlagsUIHandler::HandleRestartBrowser(const base::ListValue& args) {
  DCHECK(flags_storage_);
#if BUILDFLAG(IS_CHROMEOS)
  // On Chrome OS be less intrusive and restart inside the user session after
  // we apply the newly selected flags.
  VLOG(1) << "Restarting to apply per-session flags...";
  ash::about_flags::FeatureFlagsUpdate(*flags_storage_,
                                       Profile::FromWebUI(web_ui())->GetPrefs())
      .UpdateSessionManager();
#endif
  chrome::AttemptRestart();
}

void FlagsUIHandler::HandleResetAllFlags(const base::ListValue& args) {
  DCHECK(flags_storage_);
  about_flags::ResetAllFlags(flags_storage_.get());
}

void FlagsUIHandler::HandleExportFlags(const base::ListValue& args) {
  DCHECK(flags_storage_);
  CHECK(base::FeatureList::IsEnabled(features::kImportExportFlags));
  AllowJavascript();
  const base::Value& callback_id = args[0];

  base::DictValue results;
  base::ListValue enabled_flags;
  for (const std::string& flag : flags_storage_->GetFlags()) {
    enabled_flags.Append(flag);
  }
  results.Set("enabled_flags", std::move(enabled_flags));
  results.Set("customized_flags", flags_storage_->GetCustomizedFlags());

  ResolveJavascriptCallback(callback_id, results);
}

void FlagsUIHandler::HandleImportFlags(const base::ListValue& args) {
  DCHECK(flags_storage_);
  CHECK(base::FeatureList::IsEnabled(features::kImportExportFlags));
  AllowJavascript();
  CHECK_EQ(2u, args.size());
  const base::Value& callback_id = args[0];
  const base::DictValue& data = args[1].GetDict();

  // Apply imported data.
  if (const base::ListValue* enabled_flags_list =
          data.FindList("enabled_flags")) {
    std::set<std::string> enabled_flags = flags_storage_->GetFlags();
    for (const auto& flag : *enabled_flags_list) {
      if (flag.is_string()) {
        enabled_flags.insert(flag.GetString());
      }
    }
    flags_storage_->SetFlags(enabled_flags);
  }

  if (const base::DictValue* customized_flags =
          data.FindDict("customized_flags")) {
    base::DictValue merged_customized_flags =
        flags_storage_->GetCustomizedFlags();
    merged_customized_flags.Merge(customized_flags->Clone());
    flags_storage_->SetCustomizedFlags(merged_customized_flags);
  }

  flags_storage_->CommitPendingWrites();

  ResolveJavascriptCallback(callback_id, base::Value(true));
}
