// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/reduce_accept_language/browser/reduce_accept_language_service.h"

#include <string>

#include "base/json/json_reader.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_split.h"
#include "base/time/time.h"
#include "components/content_settings/core/browser/content_settings_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_constraints.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/language/core/browser/language_prefs.h"
#include "components/language/core/browser/pref_names.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/features.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace reduce_accept_language {

namespace {

const char kReduceAcceptLanguageSettingKey[] = "reduce-accept-language";

}  // namespace

ReduceAcceptLanguageService::ReduceAcceptLanguageService(
    HostContentSettingsMap* settings_map,
    PrefService* pref_service,
    bool is_incognito)
    : settings_map_(settings_map), is_incognito_(is_incognito) {
  DCHECK(settings_map_);

#if BUILDFLAG(IS_CHROMEOS)
  const std::string& key = language::prefs::kPreferredLanguages;
#else
  const std::string& key = language::prefs::kAcceptLanguages;
#endif
  pref_accept_language_.Init(
      key, pref_service,
      base::BindRepeating(&ReduceAcceptLanguageService::UpdateAcceptLanguage,
                          base::Unretained(this)));
  // StringPrefMember Init doesn't trigger the callback observer, we need to
  // explicitly set the accept Language after init.
  UpdateAcceptLanguage();
}

ReduceAcceptLanguageService::~ReduceAcceptLanguageService() = default;

void ReduceAcceptLanguageService::Shutdown() {
  user_accept_languages_.clear();
  pref_accept_language_.Destroy();
}

std::optional<std::string> ReduceAcceptLanguageService::GetReducedLanguage(
    const url::Origin& origin) {
  const GURL& url = origin.GetURL();

  // Only reduce accept-language in http and https scheme.
  if (!url.SchemeIsHTTPOrHTTPS())
    return std::nullopt;

  // Record the time spent getting the reduced accept language to better
  // understand whether this prefs read can introduce any large latency.
  SCOPED_UMA_HISTOGRAM_TIMER_MICROS("ReduceAcceptLanguage.FetchLatencyUs");

  const base::Value& accept_language_rule = settings_map_->GetWebsiteSetting(
      url, GURL(), ContentSettingsType::REDUCED_ACCEPT_LANGUAGE, nullptr);

  if (accept_language_rule.is_none()) {
    return std::nullopt;
  }

  DCHECK(accept_language_rule.is_dict());

  const base::Value* language_value =
      accept_language_rule.GetDict().Find(kReduceAcceptLanguageSettingKey);
  if (language_value == nullptr) {
    return std::nullopt;
  }

  // We should guarantee reduce accept language always be Type::String since
  // we save it as string in the Prefs.
  DCHECK(language_value->is_string());
  return std::make_optional(language_value->GetString());
}

std::vector<std::string> ReduceAcceptLanguageService::GetUserAcceptLanguages()
    const {
  return user_accept_languages_;
}

void ReduceAcceptLanguageService::PersistReducedLanguage(
    const url::Origin& origin,
    const std::string& language) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  const GURL url = origin.GetURL();
  if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS())
    return;

  const base::TimeTicks start_time = base::TimeTicks::Now();

  base::DictValue accept_language_dictionary;
  base::TimeDelta cache_duration =
      network::features::kReduceAcceptLanguageCacheDuration.Get();

  accept_language_dictionary.Set(kReduceAcceptLanguageSettingKey, language);
  content_settings::ContentSettingConstraints constraints;
  constraints.set_lifetime(cache_duration);
  constraints.set_session_model(content_settings::mojom::SessionModel::DURABLE);
  settings_map_->SetWebsiteSettingDefaultScope(
      url, GURL(), ContentSettingsType::REDUCED_ACCEPT_LANGUAGE,
      base::Value(std::move(accept_language_dictionary)), constraints);

  // Record the time spent getting the reduce accept language.
  base::TimeDelta duration = base::TimeTicks::Now() - start_time;
  base::UmaHistogramTimes("ReduceAcceptLanguage.StoreLatency", duration);
  base::UmaHistogramCounts100("ReduceAcceptLanguage.UpdateSize",
                              language.size());
}

void ReduceAcceptLanguageService::ClearReducedLanguage(
    const url::Origin& origin) {
  const GURL& url = origin.GetURL();

  // Only reduce accept-language in http and https scheme.
  if (!url.SchemeIsHTTPOrHTTPS())
    return;

  SCOPED_UMA_HISTOGRAM_TIMER("ReduceAcceptLanguage.ClearLatency");

  settings_map_->SetWebsiteSettingDefaultScope(
      url, GURL(), ContentSettingsType::REDUCED_ACCEPT_LANGUAGE, base::Value());
}

void ReduceAcceptLanguageService::UpdateAcceptLanguage() {
  // In incognito mode return a genericized language list.
  std::string accept_languages_str = net::HttpUtil::ExpandLanguageList(
      is_incognito_
          ? language::GetIncognitoLanguageList(pref_accept_language_.GetValue())
          : pref_accept_language_.GetValue());
  user_accept_languages_ = base::SplitString(
      accept_languages_str, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);

  base::UmaHistogramBoolean(
      "ReduceAcceptLanguage.AcceptLanguagePrefValueIsEmpty",
      user_accept_languages_.empty());
}

}  // namespace reduce_accept_language
