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

#include <map>
#include <memory>
#include <set>
#include <utility>

#include "base/bind.h"
#include "base/callback.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "components/offline_pages/core/offline_store_utils.h"
#include "components/offline_pages/core/prefetch/prefetch_dispatcher.h"
#include "components/offline_pages/core/prefetch/prefetch_types.h"
#include "components/offline_pages/core/prefetch/store/prefetch_store.h"
#include "components/offline_pages/core/prefetch/store/prefetch_store_utils.h"
#include "sql/connection.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "url/gurl.h"

namespace offline_pages {

using Result = AddUniqueUrlsTask::Result;

namespace {

std::map<std::string, std::pair<int64_t, PrefetchItemState>>
FindExistingPrefetchItemsInNamespaceSync(sql::Connection* db,
                                         const std::string& name_space) {
  static const char kSql[] =
      "SELECT offline_id, state, requested_url FROM prefetch_items"
      " WHERE client_namespace = ?";
  sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindString(0, name_space);

  std::map<std::string, std::pair<int64_t, PrefetchItemState>> result;
  while (statement.Step()) {
    result.insert(std::make_pair(
        statement.ColumnString(2),
        std::make_pair(statement.ColumnInt64(0), static_cast<PrefetchItemState>(
                                                     statement.ColumnInt(1)))));
  }

  return result;
}

bool CreatePrefetchItemSync(sql::Connection* db,
                            const std::string& name_space,
                            const PrefetchURL& prefetch_url,
                            int64_t now_db_time) {
  static const char kSql[] =
      "INSERT INTO prefetch_items"
      " (offline_id, requested_url, client_namespace, client_id, creation_time,"
      " freshness_time, title)"
      " VALUES"
      " (?, ?, ?, ?, ?, ?, ?)";

  sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, store_utils::GenerateOfflineId());
  statement.BindString(1, prefetch_url.url.spec());
  statement.BindString(2, name_space);
  statement.BindString(3, prefetch_url.id);
  statement.BindInt64(4, now_db_time);
  statement.BindInt64(5, now_db_time);
  statement.BindString16(6, prefetch_url.title);

  return statement.Run();
}

// Adds new prefetch item entries to the store using the URLs and client IDs
// from |candidate_prefetch_urls| and the client's |name_space|. Also cleans up
// entries in the Zombie state from the client's |name_space| except for the
// ones whose URL is contained in |candidate_prefetch_urls|.
// Returns the number of added prefecth items.
Result AddUrlsAndCleanupZombiesSync(
    const std::string& name_space,
    const std::vector<PrefetchURL>& candidate_prefetch_urls,
    sql::Connection* db) {
  sql::Transaction transaction(db);
  if (!transaction.Begin())
    return Result::STORE_ERROR;

  std::map<std::string, std::pair<int64_t, PrefetchItemState>> existing_items =
      FindExistingPrefetchItemsInNamespaceSync(db, name_space);

  int added_row_count = 0;
  base::Time now = base::Time::Now();
  // Insert rows in reverse order to ensure that the beginning of the list has
  // the newest timestamp.  This will cause it to be prefetched first.
  for (auto candidate_iter = candidate_prefetch_urls.rbegin();
       candidate_iter != candidate_prefetch_urls.rend(); ++candidate_iter) {
    PrefetchURL prefetch_url = *candidate_iter;
    auto iter = existing_items.find(prefetch_url.url.spec());
    if (iter == existing_items.end()) {
      if (!CreatePrefetchItemSync(db, name_space, prefetch_url,
                                  store_utils::ToDatabaseTime(now)))
        return Result::STORE_ERROR;  // Transaction rollback.
      added_row_count++;

      // We artificially add a microsecond to ensure that the timestamp is
      // different (and guarantee a particular order when sorting by timestamp).
      now += base::TimeDelta::FromMicroseconds(1);
    } else {
      // Removing from the list of existing items if it was requested again, to
      // prevent it from being removed in the next step.
      existing_items.erase(iter);
    }
  }

  // Purge remaining zombie IDs.
  for (const auto& existing_item : existing_items) {
    if (existing_item.second.second != PrefetchItemState::ZOMBIE)
      continue;
    if (!PrefetchStoreUtils::DeletePrefetchItemByOfflineIdSync(
            db, existing_item.second.first)) {
      return Result::STORE_ERROR;  // Transaction rollback.
    }
  }

  if (!transaction.Commit())
    return Result::STORE_ERROR;  // Transaction rollback.

  UMA_HISTOGRAM_COUNTS_100("OfflinePages.Prefetching.UniqueUrlsAddedCount",
                           added_row_count);
  return added_row_count > 0 ? Result::URLS_ADDED : Result::NOTHING_ADDED;
}
}  // namespace

AddUniqueUrlsTask::AddUniqueUrlsTask(
    PrefetchDispatcher* prefetch_dispatcher,
    PrefetchStore* prefetch_store,
    const std::string& name_space,
    const std::vector<PrefetchURL>& prefetch_urls)
    : prefetch_dispatcher_(prefetch_dispatcher),
      prefetch_store_(prefetch_store),
      name_space_(name_space),
      prefetch_urls_(prefetch_urls),
      weak_ptr_factory_(this) {
  DCHECK(prefetch_dispatcher_);
  DCHECK(prefetch_store_);
}

AddUniqueUrlsTask::~AddUniqueUrlsTask() {}

void AddUniqueUrlsTask::Run() {
  prefetch_store_->Execute(base::BindOnce(&AddUrlsAndCleanupZombiesSync,
                                          name_space_, prefetch_urls_),
                           base::BindOnce(&AddUniqueUrlsTask::OnUrlsAdded,
                                          weak_ptr_factory_.GetWeakPtr()),
                           Result::STORE_ERROR);
}

void AddUniqueUrlsTask::OnUrlsAdded(Result result) {
  if (result == Result::URLS_ADDED) {
    prefetch_dispatcher_->EnsureTaskScheduled();
    prefetch_dispatcher_->SchedulePipelineProcessing();
  }
  TaskComplete();
}

}  // namespace offline_pages
