// Copyright 2014 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/domain_reliability/uploader.h"

#include <utility>

#include "base/bind.h"
#include "base/callback.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/supports_user_data.h"
#include "components/domain_reliability/util.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context_getter.h"

namespace domain_reliability {

namespace {

const char kJsonMimeType[] = "application/json; charset=utf-8";

class UploadUserData : public base::SupportsUserData::Data {
 public:
  static net::URLFetcher::CreateDataCallback CreateCreateDataCallback(
      int depth) {
    return base::Bind(&UploadUserData::CreateUploadUserData, depth);
  }

  static const void* const kUserDataKey;

  int depth() const { return depth_; }

 private:
  UploadUserData(int depth) : depth_(depth) {}

  static std::unique_ptr<base::SupportsUserData::Data> CreateUploadUserData(
      int depth) {
    return base::WrapUnique(new UploadUserData(depth));
  }

  int depth_;
};

const void* const UploadUserData::kUserDataKey =
    &UploadUserData::kUserDataKey;

class DomainReliabilityUploaderImpl
    : public DomainReliabilityUploader, net::URLFetcherDelegate {
 public:
  DomainReliabilityUploaderImpl(
      MockableTime* time,
      const scoped_refptr<net::URLRequestContextGetter>&
          url_request_context_getter)
      : time_(time),
        url_request_context_getter_(url_request_context_getter),
        discard_uploads_(true),
        shutdown_(false),
        discarded_upload_count_(0u) {}

  ~DomainReliabilityUploaderImpl() override {
    DCHECK(shutdown_);
  }

  // DomainReliabilityUploader implementation:
  void UploadReport(
      const std::string& report_json,
      int max_upload_depth,
      const GURL& upload_url,
      const DomainReliabilityUploader::UploadCallback& callback) override {
    VLOG(1) << "Uploading report to " << upload_url;
    VLOG(2) << "Report JSON: " << report_json;

    if (discard_uploads_)
      discarded_upload_count_++;

    if (discard_uploads_ || shutdown_) {
      VLOG(1) << "Discarding report instead of uploading.";
      UploadResult result;
      result.status = UploadResult::SUCCESS;
      callback.Run(result);
      return;
    }

    net::NetworkTrafficAnnotationTag traffic_annotation =
        net::DefineNetworkTrafficAnnotation("domain_reliability_report_upload",
                                            R"(
          semantics {
            sender: "Domain Reliability"
            description:
              "If Chromium has trouble reaching certain Google sites or "
              "services, Domain Reliability may report the problems back to "
              "Google."
            trigger: "Failure to load certain Google sites or services."
            data:
              "Details of the failed request, including the URL, any IP "
              "addresses the browser tried to connect to, error(s) "
              "encountered loading the resource, and other connection details."
            destination: GOOGLE_OWNED_SERVICE
          }
          policy {
            cookies_allowed: NO
            setting:
              "Users can enable or disable Domain Reliability on desktop, via "
              "toggling 'Automatically send usage statistics and crash reports "
              "to Google' in Chromium's settings under Privacy. On ChromeOS, "
              "the setting is named 'Automatically send diagnostic and usage "
              "data to Google'."
            policy_exception_justification: "Not implemented."
          })");
    std::unique_ptr<net::URLFetcher> owned_fetcher = net::URLFetcher::Create(
        0, upload_url, net::URLFetcher::POST, this, traffic_annotation);
    net::URLFetcher* fetcher = owned_fetcher.get();
    fetcher->SetRequestContext(url_request_context_getter_.get());
    fetcher->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
                          net::LOAD_DO_NOT_SAVE_COOKIES);
    fetcher->SetUploadData(kJsonMimeType, report_json);
    fetcher->SetAutomaticallyRetryOn5xx(false);
    fetcher->SetURLRequestUserData(
        UploadUserData::kUserDataKey,
        UploadUserData::CreateCreateDataCallback(max_upload_depth + 1));
    fetcher->Start();

    uploads_[fetcher] = {std::move(owned_fetcher), callback};
  }

  void SetDiscardUploads(bool discard_uploads) override {
    discard_uploads_ = discard_uploads;
    VLOG(1) << "Setting discard_uploads to " << discard_uploads;
  }

  void Shutdown() override {
    DCHECK(!shutdown_);
    shutdown_ = true;
    uploads_.clear();
  }

  int GetDiscardedUploadCount() const override {
    return discarded_upload_count_;
  }

  // net::URLFetcherDelegate implementation:
  void OnURLFetchComplete(const net::URLFetcher* fetcher) override {
    DCHECK(fetcher);

    auto callback_it = uploads_.find(fetcher);
    DCHECK(callback_it != uploads_.end());

    int net_error = GetNetErrorFromURLRequestStatus(fetcher->GetStatus());
    int http_response_code = fetcher->GetResponseCode();
    base::TimeDelta retry_after;
    {
      std::string retry_after_string;
      if (fetcher->GetResponseHeaders() &&
          fetcher->GetResponseHeaders()->EnumerateHeader(nullptr,
                                                         "Retry-After",
                                                         &retry_after_string)) {
        net::HttpUtil::ParseRetryAfterHeader(retry_after_string,
                                             time_->Now(),
                                             &retry_after);
      }
    }

    VLOG(1) << "Upload finished with net error " << net_error
            << ", response code " << http_response_code
            << ", retry after " << retry_after;

    base::UmaHistogramSparse("DomainReliability.UploadResponseCode",
                             http_response_code);
    base::UmaHistogramSparse("DomainReliability.UploadNetError", -net_error);

    UploadResult result;
    GetUploadResultFromResponseDetails(net_error,
                                       http_response_code,
                                       retry_after,
                                       &result);
    callback_it->second.second.Run(result);

    uploads_.erase(callback_it);
  }

 private:
  using DomainReliabilityUploader::UploadCallback;

  MockableTime* time_;
  scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
  std::map<const net::URLFetcher*,
           std::pair<std::unique_ptr<net::URLFetcher>, UploadCallback>>
      uploads_;
  bool discard_uploads_;
  bool shutdown_;
  int discarded_upload_count_;
};

}  // namespace

DomainReliabilityUploader::DomainReliabilityUploader() {}
DomainReliabilityUploader::~DomainReliabilityUploader() {}

// static
std::unique_ptr<DomainReliabilityUploader> DomainReliabilityUploader::Create(
    MockableTime* time,
    const scoped_refptr<net::URLRequestContextGetter>&
        url_request_context_getter) {
  return std::unique_ptr<DomainReliabilityUploader>(
      new DomainReliabilityUploaderImpl(time, url_request_context_getter));
}

// static
bool DomainReliabilityUploader::OriginatedFromDomainReliability(
    const net::URLRequest& request) {
  return request.GetUserData(UploadUserData::kUserDataKey) != nullptr;
}

// static
int DomainReliabilityUploader::GetURLRequestUploadDepth(
    const net::URLRequest& request) {
  UploadUserData* data = static_cast<UploadUserData*>(
      request.GetUserData(UploadUserData::kUserDataKey));
  if (!data)
    return 0;
  return data->depth();
}

void DomainReliabilityUploader::Shutdown() {}

}  // namespace domain_reliability
