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

#include "components/web_resource/web_resource_service.h"

#include <memory>

#include "base/bind.h"
#include "base/location.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/data_use_measurement/core/data_use_user_data.h"
#include "components/google/core/browser/google_util.h"
#include "components/prefs/pref_service.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/gurl.h"

// No anonymous namespace, because const variables automatically get internal
// linkage.
const char kInvalidDataTypeError[] =
    "Data from web resource server is missing or not valid JSON.";

const char kUnexpectedJSONFormatError[] =
    "Data from web resource server does not have expected format.";

namespace web_resource {

WebResourceService::WebResourceService(
    PrefService* prefs,
    const GURL& web_resource_server,
    const std::string& application_locale,
    const char* last_update_time_pref_name,
    int start_fetch_delay_ms,
    int cache_update_delay_ms,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    const char* disable_network_switch,
    const ParseJSONCallback& parse_json_callback,
    const net::NetworkTrafficAnnotationTag& traffic_annotation)
    : prefs_(prefs),
      resource_request_allowed_notifier_(
          new ResourceRequestAllowedNotifier(prefs, disable_network_switch)),
      fetch_scheduled_(false),
      in_fetch_(false),
      web_resource_server_(web_resource_server),
      application_locale_(application_locale),
      last_update_time_pref_name_(last_update_time_pref_name),
      start_fetch_delay_ms_(start_fetch_delay_ms),
      cache_update_delay_ms_(cache_update_delay_ms),
      url_loader_factory_(url_loader_factory),
      parse_json_callback_(parse_json_callback),
      traffic_annotation_(traffic_annotation),
      weak_ptr_factory_(this) {
  resource_request_allowed_notifier_->Init(this);
  DCHECK(prefs);
}

void WebResourceService::StartAfterDelay() {
  // If resource requests are not allowed, we'll get a callback when they are.
  if (resource_request_allowed_notifier_->ResourceRequestsAllowed())
    OnResourceRequestsAllowed();
}

WebResourceService::~WebResourceService() = default;

void WebResourceService::OnSimpleLoaderComplete(
    std::unique_ptr<std::string> response_body) {
  simple_url_loader_.reset();
  if (response_body) {
    // Calls EndFetch() on completion.
    // Full JSON parsing might spawn a utility process (for security).
    // To limit the the number of simultaneously active processes
    // (on Android in particular) we short-cut the full parsing in the case of
    // trivially "empty" JSONs.
    if (response_body->empty() || *response_body == "{}") {
      OnUnpackFinished(std::make_unique<base::DictionaryValue>());
    } else {
      parse_json_callback_.Run(*response_body,
                               base::Bind(&WebResourceService::OnUnpackFinished,
                                          weak_ptr_factory_.GetWeakPtr()),
                               base::Bind(&WebResourceService::OnUnpackError,
                                          weak_ptr_factory_.GetWeakPtr()));
    }
  } else {
    // Don't parse data if attempt to download was unsuccessful.
    // Stop loading new web resource data, and silently exit.
    // We do not call parse_json_callback_, so we need to call EndFetch()
    // ourselves.
    EndFetch();
  }
}

// Delay initial load of resource data into cache so as not to interfere
// with startup time.
void WebResourceService::ScheduleFetch(int64_t delay_ms) {
  if (fetch_scheduled_)
    return;
  fetch_scheduled_ = true;
  base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
      FROM_HERE, base::Bind(&WebResourceService::StartFetch,
                            weak_ptr_factory_.GetWeakPtr()),
      base::TimeDelta::FromMilliseconds(delay_ms));
}

void WebResourceService::SetResourceRequestAllowedNotifier(
    std::unique_ptr<ResourceRequestAllowedNotifier> notifier) {
  resource_request_allowed_notifier_ = std::move(notifier);
  resource_request_allowed_notifier_->Init(this);
}

bool WebResourceService::GetFetchScheduled() const {
  return fetch_scheduled_;
}

// Initializes the fetching of data from the resource server.  Data
// load calls OnSimpleLoaderComplete.
void WebResourceService::StartFetch() {
  // Set to false so that next fetch can be scheduled after this fetch or
  // if we receive notification that resource is allowed.
  fetch_scheduled_ = false;
  // Check whether fetching is allowed.
  if (!resource_request_allowed_notifier_->ResourceRequestsAllowed())
    return;

  // First, put our next cache load on the MessageLoop.
  ScheduleFetch(cache_update_delay_ms_);

  // Set cache update time in preferences.
  prefs_->SetString(last_update_time_pref_name_,
                    base::NumberToString(base::Time::Now().ToDoubleT()));

  // If we are still fetching data, exit.
  if (in_fetch_)
    return;
  in_fetch_ = true;

  GURL web_resource_server =
      application_locale_.empty()
          ? web_resource_server_
          : google_util::AppendGoogleLocaleParam(web_resource_server_,
                                                 application_locale_);

  DVLOG(1) << "WebResourceService StartFetch " << web_resource_server;
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = web_resource_server;
  // Do not let url fetcher affect existing state in system context
  // (by setting cookies, for example).
  resource_request->load_flags = net::LOAD_DISABLE_CACHE |
                                 net::LOAD_DO_NOT_SEND_COOKIES |
                                 net::LOAD_DO_NOT_SAVE_COOKIES;
  // TODO(https://crbug.com/808498): Re-add data use measurement once
  // SimpleURLLoader supports it.
  // ID=data_use_measurement::DataUseUserData::WEB_RESOURCE_SERVICE
  simple_url_loader_ = network::SimpleURLLoader::Create(
      std::move(resource_request), traffic_annotation_);
  simple_url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
      url_loader_factory_.get(),
      base::BindOnce(&WebResourceService::OnSimpleLoaderComplete,
                     base::Unretained(this)));
}

void WebResourceService::EndFetch() {
  in_fetch_ = false;
}

void WebResourceService::OnUnpackFinished(std::unique_ptr<base::Value> value) {
  if (!value) {
    // Page information not properly read, or corrupted.
    OnUnpackError(kInvalidDataTypeError);
    return;
  }
  const base::DictionaryValue* dict = nullptr;
  if (!value->GetAsDictionary(&dict)) {
    OnUnpackError(kUnexpectedJSONFormatError);
    return;
  }
  Unpack(*dict);

  EndFetch();
}

void WebResourceService::OnUnpackError(const std::string& error_message) {
  LOG(ERROR) << error_message;
  EndFetch();
}

void WebResourceService::OnResourceRequestsAllowed() {
  int64_t delay = start_fetch_delay_ms_;
  // Check whether we have ever put a value in the web resource cache;
  // if so, pull it out and see if it's time to update again.
  if (prefs_->HasPrefPath(last_update_time_pref_name_)) {
    std::string last_update_pref =
        prefs_->GetString(last_update_time_pref_name_);
    if (!last_update_pref.empty()) {
      double last_update_value;
      base::StringToDouble(last_update_pref, &last_update_value);
      int64_t ms_until_update =
          cache_update_delay_ms_ -
          static_cast<int64_t>(
              (base::Time::Now() - base::Time::FromDoubleT(last_update_value))
                  .InMilliseconds());
      // Wait at least |start_fetch_delay_ms_|.
      if (ms_until_update > start_fetch_delay_ms_)
        delay = ms_until_update;
    }
  }
  // Start fetch and wait for UpdateResourceCache.
  ScheduleFetch(delay);
}

}  // namespace web_resource
