// Copyright 2017 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/offline_pages/core/prefetch/metrics_finalization_task.h"

#include <algorithm>
#include <memory>
#include <vector>

#include "base/bind.h"
#include "base/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/sparse_histogram.h"
#include "components/offline_pages/core/offline_store_utils.h"
#include "components/offline_pages/core/prefetch/prefetch_types.h"
#include "components/offline_pages/core/prefetch/store/prefetch_store.h"
#include "sql/connection.h"
#include "sql/statement.h"
#include "sql/transaction.h"

namespace offline_pages {
namespace {

// In-memory representation of URL metadata fetched from SQLite storage.
struct PrefetchItemStats {
  PrefetchItemStats(int64_t offline_id,
                    int generate_bundle_attempts,
                    int get_operation_attempts,
                    int download_initiation_attempts,
                    int64_t archive_body_length,
                    base::Time creation_time,
                    PrefetchItemErrorCode error_code,
                    int64_t file_size)
      : offline_id(offline_id),
        generate_bundle_attempts(generate_bundle_attempts),
        get_operation_attempts(get_operation_attempts),
        download_initiation_attempts(download_initiation_attempts),
        archive_body_length(archive_body_length),
        creation_time(creation_time),
        error_code(error_code),
        file_size(file_size) {}

  int64_t offline_id;
  int generate_bundle_attempts;
  int get_operation_attempts;
  int download_initiation_attempts;
  int64_t archive_body_length;
  base::Time creation_time;
  PrefetchItemErrorCode error_code;
  int64_t file_size;
};

std::vector<PrefetchItemStats> FetchUrlsSync(sql::Connection* db) {
  static const char kSql[] = R"(
  SELECT offline_id, generate_bundle_attempts, get_operation_attempts,
    download_initiation_attempts, archive_body_length, creation_time,
    error_code, file_size
  FROM prefetch_items
  WHERE state = ?
)";
  sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt(0, static_cast<int>(PrefetchItemState::FINISHED));

  std::vector<PrefetchItemStats> urls;
  while (statement.Step()) {
    urls.emplace_back(statement.ColumnInt64(0),  // offline_id
                      statement.ColumnInt(1),    // generate_bundle_attempts
                      statement.ColumnInt(2),    // get_operation_attempts
                      statement.ColumnInt(3),    // download_initiation_attempts
                      statement.ColumnInt64(4),  // archive_body_length
                      store_utils::FromDatabaseTime(
                          statement.ColumnInt64(5)),  // creation_time
                      static_cast<PrefetchItemErrorCode>(
                          statement.ColumnInt(6)),  // error_code
                      statement.ColumnInt64(7)      // file_size
                      );
  }

  return urls;
}

bool MarkUrlAsZombie(sql::Connection* db,
                     base::Time freshness_time,
                     int64_t offline_id) {
  static const char kSql[] =
      "UPDATE prefetch_items SET state = ?, freshness_time = ? WHERE "
      "offline_id = ?";
  sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt(0, static_cast<int>(PrefetchItemState::ZOMBIE));
  statement.BindInt(1, store_utils::ToDatabaseTime(freshness_time));
  statement.BindInt64(2, offline_id);
  return statement.Run();
}

void LogStateCountMetrics(PrefetchItemState state, int count) {
  // The histogram below is an expansion of the UMA_HISTOGRAM_ENUMERATION
  // macro adapted to allow for setting a count.
  // Note: The factory creates and owns the histogram.
  base::HistogramBase* histogram = base::SparseHistogram::FactoryGet(
      "OfflinePages.Prefetching.StateCounts",
      base::HistogramBase::kUmaTargetedHistogramFlag);
  histogram->AddCount(static_cast<int>(state), count);
}

void CountEntriesInEachState(sql::Connection* db) {
  static const char kSql[] =
      "SELECT state, COUNT (*) FROM prefetch_items GROUP BY state";
  sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
  while (statement.Step()) {
    PrefetchItemState state =
        static_cast<PrefetchItemState>(statement.ColumnInt(0));
    int count = statement.ColumnInt(1);
    LogStateCountMetrics(state, count);
  }
}

// These constants represent important indexes in the
// OfflinePrefetchArchiveActualSizeVsExpected histogram enum.
const int kEnumZeroArchiveBodyLength = 0;
const int kEnumRange0To100Min = 1;
const int kEnumRange0To100Max = 10;
const int kEnumSizeMatches100 = 11;
const int kEnumRange100To200OrMoreMin = 12;
const int kEnumRange100To200OrMoreMax = 22;

// Value meaning the file size should not be reported (not a valid index in the
// OfflinePrefetchArchiveActualSizeVsExpected enum).
const int kShouldNotReportFileSize = -1;

// Returned values conform to the definition of the
// OfflinePrefetchArchiveActualSizeVsExpected histogram enum. Except for -1
// which means "do not report".
int GetFileSizeEnumValueFor(int64_t archive_body_length, int64_t file_size) {
  // Archiving service reported body length as zero.
  if (archive_body_length == 0)
    return kEnumZeroArchiveBodyLength;

  // For other cases, reporting should only happen if both size values were set
  // meaning the item reached at least the successfully downloaded state.
  if (archive_body_length < 0 || file_size < 0)
    return kShouldNotReportFileSize;

  if (archive_body_length == file_size)
    return kEnumSizeMatches100;

  // For smaller sizes than expected, return an index in the 0% <= ratio < 100%
  // range.
  double ratio = static_cast<double>(file_size) / archive_body_length;
  if (archive_body_length > file_size) {
    return std::max(
        kEnumRange0To100Min,
        std::min(kEnumRange0To100Max, static_cast<int>(ratio * 10) + 1));
  }

  // Otherwise return an index in the 100% < ratio range.
  return std::max(
      kEnumRange100To200OrMoreMin,
      std::min(kEnumRange100To200OrMoreMax, static_cast<int>(ratio * 10) + 2));
}

void ReportMetricsFor(const PrefetchItemStats& url, const base::Time now) {
  // Lifetime reporting.
  static const int kFourWeeksInSeconds =
      base::TimeDelta::FromDays(28).InSeconds();
  const bool successful = url.error_code == PrefetchItemErrorCode::SUCCESS;
  int64_t lifetime_seconds = (now - url.creation_time).InSeconds();
  if (successful) {
    UMA_HISTOGRAM_CUSTOM_COUNTS(
        "OfflinePages.Prefetching.ItemLifetime.Successful", lifetime_seconds, 1,
        kFourWeeksInSeconds, 50);
  } else {
    UMA_HISTOGRAM_CUSTOM_COUNTS("OfflinePages.Prefetching.ItemLifetime.Failed",
                                lifetime_seconds, 1, kFourWeeksInSeconds, 50);
  }

  // Error code reporting.
  base::UmaHistogramSparse("OfflinePages.Prefetching.FinishedItemErrorCode",
                           static_cast<int>(url.error_code));

  // Unexpected file size reporting.
  int file_size_enum_value =
      GetFileSizeEnumValueFor(url.archive_body_length, url.file_size);
  if (file_size_enum_value != kShouldNotReportFileSize) {
    UMA_HISTOGRAM_ENUMERATION(
        "OfflinePages.Prefetching.DownloadedArchiveSizeVsExpected",
        file_size_enum_value, kEnumRange100To200OrMoreMax);
  }

  // Attempt counts reporting.
  static const int kMaxPossibleRetries = 20;
  UMA_HISTOGRAM_EXACT_LINEAR(
      "OfflinePages.Prefetching.ActionAttempts.GeneratePageBundle",
      url.generate_bundle_attempts, kMaxPossibleRetries);
  UMA_HISTOGRAM_EXACT_LINEAR(
      "OfflinePages.Prefetching.ActionAttempts.GetOperation",
      url.get_operation_attempts, kMaxPossibleRetries);
  UMA_HISTOGRAM_EXACT_LINEAR(
      "OfflinePages.Prefetching.ActionAttempts.DownloadInitiation",
      url.download_initiation_attempts, kMaxPossibleRetries);
}

bool ReportMetricsAndFinalizeSync(sql::Connection* db) {
  sql::Transaction transaction(db);
  if (!transaction.Begin())
    return false;

  // Gather metrics about the current number of entries with each state.  Check
  // before zombification so that we will be able to see entries in the finished
  // state, otherwise we will only see zombies, never finished entries.
  CountEntriesInEachState(db);

  const std::vector<PrefetchItemStats> urls = FetchUrlsSync(db);

  base::Time now = base::Time::Now();
  for (const auto& url : urls) {
    MarkUrlAsZombie(db, now, url.offline_id);
  }

  if (transaction.Commit()) {
    for (const auto& url : urls) {
      DVLOG(1) << "Finalized prefetch item with error code "
               << static_cast<int>(url.error_code) << ": (" << url.offline_id
               << ", " << url.generate_bundle_attempts << ", "
               << url.get_operation_attempts << ", "
               << url.download_initiation_attempts << ", "
               << url.archive_body_length << ", " << url.creation_time << ", "
               << url.file_size << ")";
      ReportMetricsFor(url, now);
    }
    return true;
  }

  return false;
}

}  // namespace

MetricsFinalizationTask::MetricsFinalizationTask(PrefetchStore* prefetch_store)
    : prefetch_store_(prefetch_store), weak_factory_(this) {}

MetricsFinalizationTask::~MetricsFinalizationTask() {}

void MetricsFinalizationTask::Run() {
  prefetch_store_->Execute(
      base::BindOnce(&ReportMetricsAndFinalizeSync),
      base::BindOnce(&MetricsFinalizationTask::MetricsFinalized,
                     weak_factory_.GetWeakPtr()),
      false);
}

void MetricsFinalizationTask::MetricsFinalized(bool result) {
  TaskComplete();
}

}  // namespace offline_pages
