// 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/context.h"

#include <algorithm>

#include "base/bind.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/metrics/sparse_histogram.h"
#include "base/values.h"
#include "components/domain_reliability/dispatcher.h"
#include "components/domain_reliability/uploader.h"
#include "components/domain_reliability/util.h"
#include "net/base/net_errors.h"
#include "net/url_request/url_request_context_getter.h"

using base::DictionaryValue;
using base::ListValue;
using base::Value;

namespace domain_reliability {

namespace {
typedef std::deque<DomainReliabilityBeacon> BeaconDeque;
typedef BeaconDeque::iterator BeaconIterator;
typedef BeaconDeque::const_iterator BeaconConstIterator;
}  // namespace

class DomainReliabilityContext::ResourceState {
 public:
  ResourceState(DomainReliabilityContext* context,
                const DomainReliabilityConfig::Resource* config)
      : context(context),
        config(config),
        successful_requests(0),
        failed_requests(0),
        uploading_successful_requests(0),
        uploading_failed_requests(0) {}
  ~ResourceState() {}

  // Serializes the resource state into a Value to be included in an upload.
  // If there is nothing to report (no beacons and all request counters are 0),
  // returns a scoped_ptr to NULL instead so the resource can be omitted.
  scoped_ptr<base::Value> ToValue(base::TimeTicks upload_time) const {
    if (successful_requests == 0 && failed_requests == 0)
      return scoped_ptr<base::Value>();

    DictionaryValue* resource_value = new DictionaryValue();
    resource_value->SetString("name", config->name);
    resource_value->SetInteger("successful_requests", successful_requests);
    resource_value->SetInteger("failed_requests", failed_requests);

    return scoped_ptr<Value>(resource_value);
  }

  // Remembers the current state of the resource data when an upload starts.
  void MarkUpload() {
    DCHECK_EQ(0u, uploading_successful_requests);
    DCHECK_EQ(0u, uploading_failed_requests);
    uploading_successful_requests = successful_requests;
    uploading_failed_requests = failed_requests;
  }

  // Uses the state remembered by |MarkUpload| to remove successfully uploaded
  // data but keep beacons and request counts added after the upload started.
  void CommitUpload() {
    successful_requests -= uploading_successful_requests;
    failed_requests -= uploading_failed_requests;
    uploading_successful_requests = 0;
    uploading_failed_requests = 0;
  }

  void RollbackUpload() {
    uploading_successful_requests = 0;
    uploading_failed_requests = 0;
  }

  DomainReliabilityContext* context;
  const DomainReliabilityConfig::Resource* config;

  uint32 successful_requests;
  uint32 failed_requests;

  // State saved during uploads; if an upload succeeds, these are used to
  // remove uploaded data from the beacon list and request counters.
  uint32 uploading_successful_requests;
  uint32 uploading_failed_requests;

 private:
  DISALLOW_COPY_AND_ASSIGN(ResourceState);
};

// static
const size_t DomainReliabilityContext::kMaxQueuedBeacons = 150;

DomainReliabilityContext::DomainReliabilityContext(
    MockableTime* time,
    const DomainReliabilityScheduler::Params& scheduler_params,
    const std::string& upload_reporter_string,
    DomainReliabilityDispatcher* dispatcher,
    DomainReliabilityUploader* uploader,
    scoped_ptr<const DomainReliabilityConfig> config)
    : config_(config.Pass()),
      time_(time),
      upload_reporter_string_(upload_reporter_string),
      scheduler_(time,
                 config_->collectors.size(),
                 scheduler_params,
                 base::Bind(&DomainReliabilityContext::ScheduleUpload,
                            base::Unretained(this))),
      dispatcher_(dispatcher),
      uploader_(uploader),
      uploading_beacons_size_(0),
      weak_factory_(this) {
  InitializeResourceStates();
}

DomainReliabilityContext::~DomainReliabilityContext() {}

void DomainReliabilityContext::OnBeacon(const GURL& url,
                                        const DomainReliabilityBeacon& beacon) {
  size_t index = config_->GetResourceIndexForUrl(url);
  if (index == DomainReliabilityConfig::kInvalidResourceIndex)
    return;
  DCHECK_GT(states_.size(), index);

  bool success = (beacon.status == "ok");

  ResourceState* state = states_[index];
  if (success)
    ++state->successful_requests;
  else
    ++state->failed_requests;

  bool reported = false;
  bool evicted = false;
  if (state->config->DecideIfShouldReportRequest(success)) {
    beacons_.push_back(beacon);
    beacons_.back().resource = state->config->name;
    if (beacons_.size() > kMaxQueuedBeacons) {
      RemoveOldestBeacon();
      evicted = true;
    }
    scheduler_.OnBeaconAdded();
    reported = true;
    UMA_HISTOGRAM_SPARSE_SLOWLY("DomainReliability.ReportedBeaconError",
                                -beacon.chrome_error);
    // TODO(ttuttle): Histogram HTTP response code?
  }

  UMA_HISTOGRAM_BOOLEAN("DomainReliability.BeaconReported", reported);
  UMA_HISTOGRAM_BOOLEAN("DomainReliability.OnBeaconDidEvict", evicted);
}

void DomainReliabilityContext::ClearBeacons() {
  ResourceStateVector::iterator it;
  for (it = states_.begin(); it != states_.end(); ++it) {
    ResourceState* state = *it;
    state->successful_requests = 0;
    state->failed_requests = 0;
    state->uploading_successful_requests = 0;
    state->uploading_failed_requests = 0;
  }
  beacons_.clear();
  uploading_beacons_size_ = 0;
}

scoped_ptr<base::Value> DomainReliabilityContext::GetWebUIData() const {
  base::DictionaryValue* context_value = new base::DictionaryValue();

  context_value->SetString("domain", config().domain);
  context_value->SetInteger("beacon_count", static_cast<int>(beacons_.size()));
  context_value->SetInteger("uploading_beacon_count",
      static_cast<int>(uploading_beacons_size_));
  context_value->Set("scheduler", scheduler_.GetWebUIData());

  return scoped_ptr<base::Value>(context_value);
}

void DomainReliabilityContext::GetQueuedBeaconsForTesting(
    std::vector<DomainReliabilityBeacon>* beacons_out) const {
  beacons_out->assign(beacons_.begin(), beacons_.end());
}

void DomainReliabilityContext::GetRequestCountsForTesting(
    size_t resource_index,
    uint32_t* successful_requests_out,
    uint32_t* failed_requests_out) const {
  DCHECK_NE(DomainReliabilityConfig::kInvalidResourceIndex, resource_index);
  DCHECK_GT(states_.size(), resource_index);

  const ResourceState& state = *states_[resource_index];
  *successful_requests_out = state.successful_requests;
  *failed_requests_out = state.failed_requests;
}

void DomainReliabilityContext::InitializeResourceStates() {
  ScopedVector<DomainReliabilityConfig::Resource>::const_iterator it;
  for (it = config_->resources.begin(); it != config_->resources.end(); ++it)
    states_.push_back(new ResourceState(this, *it));
}

void DomainReliabilityContext::ScheduleUpload(
    base::TimeDelta min_delay,
    base::TimeDelta max_delay) {
  dispatcher_->ScheduleTask(
      base::Bind(
          &DomainReliabilityContext::StartUpload,
          weak_factory_.GetWeakPtr()),
      min_delay,
      max_delay);
}

void DomainReliabilityContext::StartUpload() {
  MarkUpload();

  DCHECK(upload_time_.is_null());
  upload_time_ = time_->NowTicks();
  std::string report_json;
  scoped_ptr<const Value> report_value(CreateReport(upload_time_));
  base::JSONWriter::Write(report_value.get(), &report_json);
  report_value.reset();

  size_t collector_index = scheduler_.OnUploadStart();

  uploader_->UploadReport(
      report_json,
      config_->collectors[collector_index]->upload_url,
      base::Bind(
          &DomainReliabilityContext::OnUploadComplete,
          weak_factory_.GetWeakPtr()));

  UMA_HISTOGRAM_BOOLEAN("DomainReliability.UploadFailover",
                        collector_index > 0);
  if (!last_upload_time_.is_null()) {
    UMA_HISTOGRAM_LONG_TIMES("DomainReliability.UploadInterval",
                             upload_time_ - last_upload_time_);
  }
}

void DomainReliabilityContext::OnUploadComplete(bool success) {
  if (success)
    CommitUpload();
  else
    RollbackUpload();
  scheduler_.OnUploadComplete(success);
  UMA_HISTOGRAM_BOOLEAN("DomainReliability.UploadSuccess", success);
  DCHECK(!upload_time_.is_null());
  UMA_HISTOGRAM_MEDIUM_TIMES("DomainReliability.UploadDuration",
                             time_->NowTicks() - upload_time_);
  last_upload_time_ = upload_time_;
  upload_time_ = base::TimeTicks();
}

scoped_ptr<const Value> DomainReliabilityContext::CreateReport(
    base::TimeTicks upload_time) const {
  scoped_ptr<ListValue> beacons_value(new ListValue());
  for (BeaconConstIterator it = beacons_.begin(); it != beacons_.end(); ++it)
    beacons_value->Append(it->ToValue(upload_time));

  scoped_ptr<ListValue> resources_value(new ListValue());
  for (ResourceStateIterator it = states_.begin(); it != states_.end(); ++it) {
    scoped_ptr<Value> resource_report = (*it)->ToValue(upload_time);
    if (resource_report)
      resources_value->Append(resource_report.release());
  }

  DictionaryValue* report_value = new DictionaryValue();
  if (!config().version.empty())
    report_value->SetString("config_version", config().version);
  report_value->SetString("reporter", upload_reporter_string_);
  report_value->Set("entries", beacons_value.release());
  if (!resources_value->empty())
    report_value->Set("resources", resources_value.release());

  return scoped_ptr<const Value>(report_value);
}

void DomainReliabilityContext::MarkUpload() {
  for (ResourceStateIterator it = states_.begin(); it != states_.end(); ++it)
    (*it)->MarkUpload();
  DCHECK_EQ(0u, uploading_beacons_size_);
  uploading_beacons_size_ = beacons_.size();
  DCHECK_NE(0u, uploading_beacons_size_);
}

void DomainReliabilityContext::CommitUpload() {
  for (ResourceStateIterator it = states_.begin(); it != states_.end(); ++it)
    (*it)->CommitUpload();
  BeaconIterator begin = beacons_.begin();
  BeaconIterator end = begin + uploading_beacons_size_;
  beacons_.erase(begin, end);
  DCHECK_NE(0u, uploading_beacons_size_);
  uploading_beacons_size_ = 0;
}

void DomainReliabilityContext::RollbackUpload() {
  for (ResourceStateIterator it = states_.begin(); it != states_.end(); ++it)
    (*it)->RollbackUpload();
  DCHECK_NE(0u, uploading_beacons_size_);
  uploading_beacons_size_ = 0;
}

void DomainReliabilityContext::RemoveOldestBeacon() {
  DCHECK(!beacons_.empty());

  VLOG(1) << "Beacon queue for " << config().domain << " full; "
          << "removing oldest beacon";

  beacons_.pop_front();

  // If that just removed a beacon counted in uploading_beacons_size_, decrement
  // that.
  if (uploading_beacons_size_ > 0)
    --uploading_beacons_size_;
}

}  // namespace domain_reliability