// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/predictors/resource_prefetch_predictor_tables.h"

#include <algorithm>
#include <utility>

#include "base/functional/bind.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.h"
#include "chrome/browser/predictors/predictors_features.h"
#include "sql/statement.h"
#include "sql/transaction.h"

namespace {

const char kMetadataTableName[] = "resource_prefetch_predictor_metadata";
const char kHostRedirectTableName[] =
    "resource_prefetch_predictor_host_redirect";
const char kOriginTableName[] = "resource_prefetch_predictor_origin";

const char kCreateGlobalMetadataStatementTemplate[] =
    "CREATE TABLE %s ( "
    "key TEXT, value INTEGER, "
    "PRIMARY KEY (key))";
const char kCreateProtoTableStatementTemplate[] =
    "CREATE TABLE %s ( "
    "key TEXT, "
    "proto BLOB, "
    "PRIMARY KEY(key))";

}  // namespace

namespace predictors {

// static
void ResourcePrefetchPredictorTables::TrimRedirects(
    RedirectData* data,
    size_t max_consecutive_misses) {
  auto new_end =
      std::remove_if(data->mutable_redirect_endpoints()->begin(),
                     data->mutable_redirect_endpoints()->end(),
                     [max_consecutive_misses](const RedirectStat& x) {
                       return x.consecutive_misses() >= max_consecutive_misses;
                     });
  data->mutable_redirect_endpoints()->erase(
      new_end, data->mutable_redirect_endpoints()->end());
}

// static
void ResourcePrefetchPredictorTables::TrimOrigins(
    OriginData* data,
    size_t max_consecutive_misses) {
  auto* origins = data->mutable_origins();
  auto new_end = std::remove_if(
      origins->begin(), origins->end(), [=](const OriginStat& x) {
        return x.consecutive_misses() >= max_consecutive_misses;
      });
  origins->erase(new_end, origins->end());
}

// static
void ResourcePrefetchPredictorTables::SortOrigins(
    OriginData* data,
    const std::string& main_frame_origin) {
  auto* origins = data->mutable_origins();
  auto it = std::ranges::find(*origins, main_frame_origin, &OriginStat::origin);
  int iterator_offset = 0;
  if (it != origins->end()) {
    origins->SwapElements(0, it - origins->begin());
    iterator_offset = 1;
  }
  std::sort(origins->begin() + iterator_offset, origins->end(),
            [](const OriginStat& x, const OriginStat& y) {
              // Decreasing score ordering.
              return ComputeOriginScore(x) > ComputeOriginScore(y);
            });
}

ResourcePrefetchPredictorTables::ResourcePrefetchPredictorTables(
    scoped_refptr<base::SequencedTaskRunner> db_task_runner)
    : sqlite_proto::TableManager(db_task_runner) {
  host_redirect_table_ =
      std::make_unique<sqlite_proto::KeyValueTable<RedirectData>>(
          kHostRedirectTableName);
  origin_table_ = std::make_unique<sqlite_proto::KeyValueTable<OriginData>>(
      kOriginTableName);
}

ResourcePrefetchPredictorTables::~ResourcePrefetchPredictorTables() = default;

// static
float ResourcePrefetchPredictorTables::ComputeOriginScore(
    const OriginStat& origin) {
  // The ranking is done by considering, in this order:
  // 1. High confidence resources (>75% and more than 10 hits)
  // 2. Mandatory network access
  // 3. Network accessed
  // 4. Average position (decreasing)
  float score = 0;
  float confidence = static_cast<float>(origin.number_of_hits()) /
                     (origin.number_of_hits() + origin.number_of_misses());

  bool is_high_confidence = confidence > .75 && origin.number_of_hits() > 10;
  score += is_high_confidence * 1e6;

  if (!base::FeatureList::IsEnabled(
          features::kLoadingPredictorDisregardAlwaysAccessesNetwork)) {
    score += origin.always_access_network() * 1e4;
  }

  score += origin.accessed_network() * 1e2;
  score += 1e2 - origin.average_position();

  return score;
}

sqlite_proto::KeyValueTable<RedirectData>*
ResourcePrefetchPredictorTables::host_redirect_table() {
  return host_redirect_table_.get();
}
sqlite_proto::KeyValueTable<OriginData>*
ResourcePrefetchPredictorTables::origin_table() {
  return origin_table_.get();
}

// static
bool ResourcePrefetchPredictorTables::DropTablesIfOutdated(sql::Database* db) {
  int version = GetDatabaseVersion(db);
  bool success = true;
  // Too new is also a problem.
  bool incompatible_version = version != kDatabaseVersion;

  // These are deprecated tables but they still have to be removed if present.
  static const char kUrlMetadataTableName[] =
      "resource_prefetch_predictor_url_metadata";
  static const char kHostMetadataTableName[] =
      "resource_prefetch_predictor_host_metadata";
  static const char kManifestTableName[] =
      "resource_prefetch_predictor_manifest";
  static const char kUrlResourceTableName[] = "resource_prefetch_predictor_url";
  static const char kUrlRedirectTableName[] =
      "resource_prefetch_predictor_url_redirect";
  static const char kHostResourceTableName[] =
      "resource_prefetch_predictor_host";

  if (incompatible_version) {
    for (const char* table_name :
         {kMetadataTableName, kUrlResourceTableName, kHostResourceTableName,
          kUrlRedirectTableName, kHostRedirectTableName, kManifestTableName,
          kUrlMetadataTableName, kHostMetadataTableName, kOriginTableName}) {
      success = success && db->Execute(base::StringPrintf(
                               "DROP TABLE IF EXISTS %s", table_name));
    }
  }

  if (incompatible_version) {
    success = success &&
              db->Execute(base::StringPrintf(
                  kCreateGlobalMetadataStatementTemplate, kMetadataTableName));
    success = success && SetDatabaseVersion(db, kDatabaseVersion);
  }

  return success;
}

// static
int ResourcePrefetchPredictorTables::GetDatabaseVersion(sql::Database* db) {
  int version = 0;
  if (db->DoesTableExist(kMetadataTableName)) {
    sql::Statement statement(db->GetUniqueStatement(base::StringPrintf(
        "SELECT value FROM %s WHERE key='version'", kMetadataTableName)));
    if (statement.Step())
      version = statement.ColumnInt(0);
  }
  return version;
}

// static
bool ResourcePrefetchPredictorTables::SetDatabaseVersion(sql::Database* db,
                                                         int version) {
  sql::Statement statement(db->GetUniqueStatement(base::StringPrintf(
      "INSERT OR REPLACE INTO %s (key,value) VALUES ('version',%d)",
      kMetadataTableName, version)));
  return statement.Run();
}

void ResourcePrefetchPredictorTables::CreateOrClearTablesIfNecessary() {
  // TODO(crbug.com/40778330): This method's logic is almost identical to
  // sqlite_proto::ProtoTableManager::CreateOrClearTablesIfNecessary, so the two
  // classes could probably share a common implementation wrapping
  // sql::MetaTable.

  DCHECK(GetTaskRunner()->RunsTasksInCurrentSequence());
  if (CantAccessDatabase())
    return;

  // Database initialization is all-or-nothing.
  sql::Database* db = DB();
  sql::Transaction transaction(db);
  bool success = transaction.Begin();
  success = success && DropTablesIfOutdated(db);

  for (const char* table_name : {kHostRedirectTableName, kOriginTableName}) {
    success = success && (db->DoesTableExist(table_name) ||
                          db->Execute(base::StringPrintf(
                              kCreateProtoTableStatementTemplate, table_name)));
  }
  success &= LcppDataMap::CreateOrClearTablesIfNecessary(db);

  if (success) {
    success = transaction.Commit();
  } else {
    transaction.Rollback();
  }

  if (!success)
    ResetDB();
}

void ResourcePrefetchPredictorTables::LogDatabaseStats() {
  DCHECK(GetTaskRunner()->RunsTasksInCurrentSequence());
  if (CantAccessDatabase())
    return;

  // TODO(alexilin): Add existing tables stats.
}

}  // namespace predictors
