// Copyright 2013 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/precache/content/precache_manager.h"

#include <string>
#include <utility>
#include <vector>

#include "base/bind.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.h"
#include "components/history/core/browser/history_service.h"
#include "components/precache/core/precache_database.h"
#include "components/precache/core/precache_switches.h"
#include "components/precache/core/proto/unfinished_work.pb.h"
#include "components/prefs/pref_service.h"
#include "components/sync/driver/sync_service.h"
#include "components/variations/metrics_util.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/network_change_notifier.h"
#include "net/http/http_cache.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"

using content::BrowserThread;

namespace precache {

const char kPrecacheFieldTrialName[] = "Precache";
const char kMinCacheSizeParam[] = "min_cache_size";

namespace {

const char kPrecacheFieldTrialEnabledGroup[] = "Enabled";
const char kPrecacheFieldTrialControlGroup[] = "Control";
const char kConfigURLParam[] = "config_url";
const char kManifestURLPrefixParam[] = "manifest_url_prefix";
const char kDataReductionProxyParam[] = "disable_if_data_reduction_proxy";
const size_t kNumTopHosts = 100;

}  // namespace

size_t NumTopHosts() {
  return kNumTopHosts;
}

PrecacheManager::PrecacheManager(
    content::BrowserContext* browser_context,
    const syncer::SyncService* const sync_service,
    const history::HistoryService* const history_service,
    const data_reduction_proxy::DataReductionProxySettings*
        data_reduction_proxy_settings,
    const base::FilePath& db_path,
    std::unique_ptr<PrecacheDatabase> precache_database)
    : browser_context_(browser_context),
      sync_service_(sync_service),
      history_service_(history_service),
      data_reduction_proxy_settings_(data_reduction_proxy_settings),
      is_precaching_(false) {
  precache_database_ = std::move(precache_database);
  BrowserThread::PostTask(
      BrowserThread::DB, FROM_HERE,
      base::Bind(base::IgnoreResult(&PrecacheDatabase::Init),
                 base::Unretained(precache_database_.get()), db_path));
}

PrecacheManager::~PrecacheManager() {
  // DeleteSoon posts a non-nestable task to the task runner, so any previously
  // posted tasks that rely on an Unretained precache_database_ will finish
  // before it is deleted.
  BrowserThread::DeleteSoon(BrowserThread::DB, FROM_HERE,
                            precache_database_.release());
}

bool PrecacheManager::IsInExperimentGroup() const {
  // Verify IsPrecachingAllowed() before calling FieldTrialList::FindFullName().
  // This is because field trials are only assigned when requested. This allows
  // us to create Control and Experiment groups that are limited to users for
  // whom PrecachingAllowed() is true, thus accentuating the impact of
  // precaching.
  return IsPrecachingAllowed() &&
         (base::StartsWith(
              base::FieldTrialList::FindFullName(kPrecacheFieldTrialName),
              kPrecacheFieldTrialEnabledGroup, base::CompareCase::SENSITIVE) ||
          base::CommandLine::ForCurrentProcess()->HasSwitch(
              switches::kEnablePrecache));
}

bool PrecacheManager::IsInControlGroup() const {
  // Verify IsPrecachingAllowed() before calling FindFullName(). See
  // PrecacheManager::IsInExperimentGroup() for an explanation of why.
  return IsPrecachingAllowed() &&
         base::StartsWith(
             base::FieldTrialList::FindFullName(kPrecacheFieldTrialName),
             kPrecacheFieldTrialControlGroup, base::CompareCase::SENSITIVE);
}

bool PrecacheManager::IsPrecachingAllowed() const {
  return PrecachingAllowed() == AllowedType::ALLOWED;
}

PrecacheManager::AllowedType PrecacheManager::PrecachingAllowed() const {
  bool disable_if_proxy = !variations::GetVariationParamValue(
      kPrecacheFieldTrialName, kDataReductionProxyParam).empty();
  if (disable_if_proxy &&
      (!data_reduction_proxy_settings_ ||
       data_reduction_proxy_settings_->IsDataReductionProxyEnabled()))
    return AllowedType::DISALLOWED;

  if (!(sync_service_ && sync_service_->IsBackendInitialized()))
    return AllowedType::PENDING;

  // SyncService delegates to SyncPrefs, which must be called on the UI thread.
  if (history_service_ &&
      sync_service_->GetActiveDataTypes().Has(syncer::SESSIONS) &&
      !sync_service_->GetEncryptedDataTypes().Has(syncer::SESSIONS))
    return AllowedType::ALLOWED;

  return AllowedType::DISALLOWED;
}

void PrecacheManager::OnCacheBackendReceived(int net_error_code) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  if (net_error_code != net::OK) {
    // Assume there is no cache.
    cache_backend_ = nullptr;
    OnCacheSizeReceived(0);
    return;
  }
  DCHECK(cache_backend_);
  int result = cache_backend_->CalculateSizeOfAllEntries(base::Bind(
      &PrecacheManager::OnCacheSizeReceived, base::Unretained(this)));
  if (result == net::ERR_IO_PENDING) {
    // Wait for the callback.
  } else if (result >= 0) {
    // The result is the expected bytes already.
    OnCacheSizeReceived(result);
  } else {
    // Error occurred. Couldn't get the size. Assume there is no cache.
    OnCacheSizeReceived(0);
  }
  cache_backend_ = nullptr;
}

void PrecacheManager::OnCacheSizeReceived(int cache_size_bytes) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(&PrecacheManager::OnCacheSizeReceivedInUIThread,
                 base::Unretained(this), cache_size_bytes));
}

void PrecacheManager::OnCacheSizeReceivedInUIThread(int cache_size_bytes) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  UMA_HISTOGRAM_MEMORY_KB("Precache.CacheSize.AllEntries",
                          cache_size_bytes / 1024);

  if (cache_size_bytes < min_cache_size_bytes_) {
    OnDone();  // Do not continue.
  } else {
    BrowserThread::PostTaskAndReplyWithResult(
        BrowserThread::DB, FROM_HERE,
        base::Bind(&PrecacheDatabase::GetUnfinishedWork,
                   base::Unretained(precache_database_.get())),
        base::Bind(&PrecacheManager::OnGetUnfinishedWorkDone, AsWeakPtr()));
  }
}

void PrecacheManager::PrecacheIfCacheIsBigEnough(
    scoped_refptr<net::URLRequestContextGetter> url_request_context_getter) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  CHECK(url_request_context_getter);

  // Continue with OnGetUnfinishedWorkDone only if the size of the cache is
  // at least min_cache_size_bytes_.
  // Class disk_cache::Backend does not expose its maximum size. However, caches
  // are usually full, so we can use the size of all the entries stored in the
  // cache (via CalculateSizeOfAllEntries) as a proxy of its maximum size.
  net::URLRequestContext* context =
      url_request_context_getter->GetURLRequestContext();
  if (!context) {
    OnCacheSizeReceived(0);
    return;
  }
  net::HttpTransactionFactory* factory = context->http_transaction_factory();
  if (!factory) {
    OnCacheSizeReceived(0);
    return;
  }
  net::HttpCache* cache = factory->GetCache();
  if (!cache) {
    // There is no known cache. Assume that there is no cache.
    // TODO(jamartin): I'm not sure this can be an actual posibility. Consider
    // making this a CHECK(cache).
    OnCacheSizeReceived(0);
    return;
  }
  const int net_error_code = cache->GetBackend(
      &cache_backend_, base::Bind(&PrecacheManager::OnCacheBackendReceived,
                                  base::Unretained(this)));
  if (net_error_code != net::ERR_IO_PENDING) {
    // No need to wait for the callback. The callback hasn't been called with
    // the appropriate code, so we call it directly.
    OnCacheBackendReceived(net_error_code);
  }
}

void PrecacheManager::StartPrecaching(
    const PrecacheCompletionCallback& precache_completion_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (is_precaching_) {
    DLOG(WARNING) << "Cannot start precaching because precaching is already "
                     "in progress.";
    return;
  }
  precache_completion_callback_ = precache_completion_callback;

  is_precaching_ = true;
  BrowserThread::PostTask(
      BrowserThread::DB, FROM_HERE,
      base::Bind(&PrecacheDatabase::SetLastPrecacheTimestamp,
                 base::Unretained(precache_database_.get()),
                 base::Time::Now()));

  // Ignore boolean return value. In all documented failure cases, it sets the
  // int to a reasonable value.
  base::StringToInt(variations::GetVariationParamValue(kPrecacheFieldTrialName,
                                                       kMinCacheSizeParam),
                    &min_cache_size_bytes_);
  if (min_cache_size_bytes_ <= 0) {
    // Skip looking up the cache size, because it doesn't matter.
    OnCacheSizeReceivedInUIThread(0);
    return;
  }

  scoped_refptr<net::URLRequestContextGetter> url_request_context_getter(
      content::BrowserContext::GetDefaultStoragePartition(browser_context_)
          ->GetURLRequestContext());
  if (!url_request_context_getter) {
    OnCacheSizeReceivedInUIThread(0);
    return;
  }

  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      base::Bind(&PrecacheManager::PrecacheIfCacheIsBigEnough, AsWeakPtr(),
                 std::move(url_request_context_getter)));
}

void PrecacheManager::OnGetUnfinishedWorkDone(
    std::unique_ptr<PrecacheUnfinishedWork> unfinished_work) {
  // Reset progress on a prefetch that has taken too long to complete.
  if (unfinished_work->has_start_time() &&
      base::Time::Now() -
              base::Time::FromInternalValue(unfinished_work->start_time()) >
          base::TimeDelta::FromHours(6)) {
    PrecacheFetcher::RecordCompletionStatistics(
        *unfinished_work, unfinished_work->top_host_size(),
        unfinished_work->resource_size());
    unfinished_work.reset(new PrecacheUnfinishedWork);
  }
  // If this prefetch is new, set the start time.
  if (!unfinished_work->has_start_time())
    unfinished_work->set_start_time(base::Time::Now().ToInternalValue());
  unfinished_work_ = std::move(unfinished_work);
  bool needs_top_hosts = unfinished_work_->top_host_size() == 0;

  if (IsInExperimentGroup()) {
    BrowserThread::PostTask(
        BrowserThread::DB, FROM_HERE,
        base::Bind(&PrecacheDatabase::DeleteExpiredPrecacheHistory,
                   base::Unretained(precache_database_.get()),
                   base::Time::Now()));

    // Request NumTopHosts() top hosts. Note that PrecacheFetcher is further
    // bound by the value of PrecacheConfigurationSettings.top_sites_count, as
    // retrieved from the server.
    if (needs_top_hosts) {
      history_service_->TopHosts(
          NumTopHosts(),
          base::Bind(&PrecacheManager::OnHostsReceived, AsWeakPtr()));
    } else {
      InitializeAndStartFetcher();
    }
  } else if (IsInControlGroup()) {
    // Calculate TopHosts solely for metrics purposes.
    if (needs_top_hosts) {
      history_service_->TopHosts(
          NumTopHosts(),
          base::Bind(&PrecacheManager::OnHostsReceivedThenDone, AsWeakPtr()));
    } else {
      OnDone();
    }
  } else {
    if (PrecachingAllowed() != AllowedType::PENDING) {
      // We are not waiting on the sync backend to be initialized. The user
      // either is not in the field trial, or does not have sync enabled.
      // Pretend that precaching started, so that the PrecacheServiceLauncher
      // doesn't try to start it again.
    }

    OnDone();
  }
}

void PrecacheManager::CancelPrecaching() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!is_precaching_) {
    // Do nothing if precaching is not in progress.
    return;
  }
  is_precaching_ = false;
  // If cancellation occurs after StartPrecaching but before OnHostsReceived,
  // is_precaching will be true, but the precache_fetcher_ will not yet be
  // constructed.
  if (precache_fetcher_) {
    std::unique_ptr<PrecacheUnfinishedWork> unfinished_work =
        precache_fetcher_->CancelPrecaching();
    if (unfinished_work) {
      BrowserThread::PostTask(BrowserThread::DB, FROM_HERE,
                              base::Bind(&PrecacheDatabase::SaveUnfinishedWork,
                                         precache_database_->GetWeakPtr(),
                                         base::Passed(&unfinished_work)));
    }
    // Destroying the |precache_fetcher_| will cancel any fetch in progress.
    precache_fetcher_.reset();
  }
  precache_completion_callback_.Reset();
}

bool PrecacheManager::IsPrecaching() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return is_precaching_;
}

void PrecacheManager::UpdatePrecacheMetricsAndState(
    const GURL& url,
    const GURL& referrer,
    const base::TimeDelta& latency,
    const base::Time& fetch_time,
    const net::HttpResponseInfo& info,
    int64_t size,
    bool is_user_traffic,
    const base::Callback<void(base::Time)>& register_synthetic_trial) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  BrowserThread::PostTaskAndReplyWithResult(
      BrowserThread::DB, FROM_HERE,
      base::Bind(&PrecacheDatabase::GetLastPrecacheTimestamp,
                 base::Unretained(precache_database_.get())),
      base::Bind(&PrecacheManager::RecordStatsForFetch, AsWeakPtr(), url,
                 referrer, latency, fetch_time, info, size,
                 register_synthetic_trial));

  if (is_user_traffic && IsPrecaching())
    CancelPrecaching();
}

void PrecacheManager::RecordStatsForFetch(
    const GURL& url,
    const GURL& referrer,
    const base::TimeDelta& latency,
    const base::Time& fetch_time,
    const net::HttpResponseInfo& info,
    int64_t size,
    const base::Callback<void(base::Time)>& register_synthetic_trial,
    base::Time last_precache_time) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  register_synthetic_trial.Run(last_precache_time);

  if (size == 0 || url.is_empty() || !url.SchemeIsHTTPOrHTTPS()) {
    // Ignore empty responses, empty URLs, or URLs that aren't HTTP or HTTPS.
    return;
  }

  if (!history_service_)
    return;

  history_service_->HostRankIfAvailable(
      referrer,
      base::Bind(&PrecacheManager::RecordStatsForFetchInternal, AsWeakPtr(),
                 url, referrer.host(), latency, fetch_time, info, size));
}

void PrecacheManager::RecordStatsForFetchInternal(
    const GURL& url,
    const std::string& referrer_host,
    const base::TimeDelta& latency,
    const base::Time& fetch_time,
    const net::HttpResponseInfo& info,
    int64_t size,
    int host_rank) {
  if (is_precaching_) {
    // Assume that precache is responsible for all requests made while
    // precaching is currently in progress.
    // TODO(sclittle): Make PrecacheFetcher explicitly mark precache-motivated
    // fetches, and use that to determine whether or not a fetch was motivated
    // by precaching.
    BrowserThread::PostTask(
        BrowserThread::DB, FROM_HERE,
        base::Bind(&PrecacheDatabase::RecordURLPrefetchMetrics,
                   base::Unretained(precache_database_.get()), info, latency));
  } else {
    bool is_connection_cellular =
        net::NetworkChangeNotifier::IsConnectionCellular(
            net::NetworkChangeNotifier::GetConnectionType());

    BrowserThread::PostTask(
        BrowserThread::DB, FROM_HERE,
        base::Bind(&PrecacheDatabase::RecordURLNonPrefetch,
                   base::Unretained(precache_database_.get()), url, latency,
                   fetch_time, info, size, host_rank, is_connection_cellular));
  }
}

void PrecacheManager::ClearHistory() {
  // PrecacheDatabase::ClearHistory must run after PrecacheDatabase::Init has
  // finished. Using PostNonNestableTask guarantees this, by definition. See
  // base::SequencedTaskRunner for details.
  BrowserThread::PostNonNestableTask(
      BrowserThread::DB, FROM_HERE,
      base::Bind(&PrecacheDatabase::ClearHistory,
                 base::Unretained(precache_database_.get())));
}

void PrecacheManager::Shutdown() {
  CancelPrecaching();
}

void PrecacheManager::OnDone() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  precache_fetcher_.reset();

  // Run completion callback if not null. It's null if the client is in the
  // Control group and CancelPrecaching is called before TopHosts computation
  // finishes.
  if (!precache_completion_callback_.is_null()) {
    precache_completion_callback_.Run(!is_precaching_);
    // Uninitialize the callback so that any scoped_refptrs in it are released.
    precache_completion_callback_.Reset();
  }

  is_precaching_ = false;
}

void PrecacheManager::OnHostsReceived(
    const history::TopHostsList& host_counts) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  for (const auto& host_count : host_counts) {
    TopHost* top_host = unfinished_work_->add_top_host();
    top_host->set_hostname(host_count.first);
    top_host->set_visits(host_count.second);
  }
  InitializeAndStartFetcher();
}

void PrecacheManager::InitializeAndStartFetcher() {
  if (!is_precaching_) {
    // Don't start precaching if it was canceled while waiting for the list of
    // hosts.
    return;
  }
  // Start precaching.
  precache_fetcher_.reset(new PrecacheFetcher(
      content::BrowserContext::GetDefaultStoragePartition(browser_context_)
          ->GetURLRequestContext(),
      GURL(variations::GetVariationParamValue(kPrecacheFieldTrialName,
                                              kConfigURLParam)),
      variations::GetVariationParamValue(kPrecacheFieldTrialName,
                                         kManifestURLPrefixParam),
      std::move(unfinished_work_),
      metrics::HashName(
          base::FieldTrialList::FindFullName(kPrecacheFieldTrialName)),
      precache_database_->GetWeakPtr(),
      content::BrowserThread::GetTaskRunnerForThread(
          content::BrowserThread::DB),
      this));
  precache_fetcher_->Start();
}

void PrecacheManager::OnHostsReceivedThenDone(
    const history::TopHostsList& host_counts) {
  OnDone();
}

}  // namespace precache
