// 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 "components/affiliations/core/browser/affiliation_database.h"

#include <stdint.h>

#include <memory>

#include <set>
#include <vector>

#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/parameter_pack.h"
#include "build/build_config.h"
#include "components/affiliations/core/browser/sql_table_builder.h"
#include "components/affiliations/core/browser/affiliation_utils.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"

namespace affiliations {

namespace {

// The current version number of the affiliation database schema.
const int kVersion = 7;

// The oldest version of the schema such that a legacy Chrome client using that
// version can still read/write the current database.
const int kCompatibleVersion = 1;

// Struct to hold table builder for "eq_classes", "eq_class_members",
// and "eq_class_groups" tables.
struct SQLTableBuilders {
  std::vector<SQLTableBuilder*> AsVector() const {
    // It's important to keep builders in this order as tables are migrated one
    // by one.
    return {eq_classes, eq_class_members, eq_class_groups, psl_extensions};
  }

  raw_ptr<SQLTableBuilder> eq_classes;
  raw_ptr<SQLTableBuilder> eq_class_members;
  raw_ptr<SQLTableBuilder> eq_class_groups;
  raw_ptr<SQLTableBuilder> psl_extensions;
};

// Seals the version of the given builders. This is method should be always used
// to seal versions of all builder to make sure all builders are at the same
// version.
void SealVersion(SQLTableBuilders builders, unsigned expected_version) {
  unsigned eq_classes_version = builders.eq_classes->SealVersion();
  DCHECK_EQ(expected_version, eq_classes_version);

  unsigned eq_class_members_version = builders.eq_class_members->SealVersion();
  DCHECK_EQ(expected_version, eq_class_members_version);

  unsigned eq_class_groups_version = builders.eq_class_groups->SealVersion();
  DCHECK_EQ(expected_version, eq_class_groups_version);

  unsigned eq_psl_extensions_version = builders.psl_extensions->SealVersion();
  DCHECK_EQ(expected_version, eq_psl_extensions_version);
}

// Initializes the passed in table builders and defines the structure of the
// tables.
void InitializeTableBuilders(SQLTableBuilders builders) {
  // Version 0 and 1 of the affiliation database.
  builders.eq_classes->AddPrimaryKeyColumn("id");
  builders.eq_classes->AddColumn("last_update_time", "INTEGER");
  builders.eq_class_members->AddPrimaryKeyColumn("id");
  builders.eq_class_members->AddColumnToUniqueKey("facet_uri",
                                                  "LONGVARCHAR NOT NULL");
  builders.eq_class_members->AddColumn(
      "set_id", "INTEGER NOT NULL REFERENCES eq_classes(id) ON DELETE CASCADE");
  // An index on eq_class_members.facet_uri is automatically created due to the
  // UNIQUE constraint, however, we must create one on eq_class_members.set_id
  // manually (to prevent linear scan when joining).
  builders.eq_class_members->AddIndex("index_on_eq_class_members_set_id",
                                      {"set_id"});
  SealVersion(builders, /*expected_version=*/0u);
  SealVersion(builders, /*expected_version=*/1u);

  // Version 2 of the affiliation database.
  builders.eq_class_members->AddColumn("facet_display_name", "VARCHAR");
  builders.eq_class_members->AddColumn("facet_icon_url", "VARCHAR");
  SealVersion(builders, /*expected_version=*/2u);

  // Version 3 of the affiliation database.
  builders.eq_class_groups->AddPrimaryKeyColumn("id");
  builders.eq_class_groups->AddColumn("facet_uri", "LONGVARCHAR NOT NULL");
  builders.eq_class_groups->AddColumn(
      "set_id", "INTEGER NOT NULL REFERENCES eq_classes(id) ON DELETE CASCADE");
  builders.eq_classes->AddColumn("group_display_name", "VARCHAR");
  builders.eq_classes->AddColumn("group_icon_url", "VARCHAR");
  SealVersion(builders, /*expected_version=*/3u);

  // Version 4 of the affiliation database.
  builders.eq_class_groups->AddColumn("main_domain", "VARCHAR");
  SealVersion(builders, /*expected_version=*/4u);

  builders.psl_extensions->AddColumnToUniqueKey("domain", "VARCHAR NOT NULL");
  SealVersion(builders, /*expected_version=*/5u);

  // Add index on eq_class_groups.facet_uri and eq_class_groups.set_id
  // manually (to prevent linear scan when joining).
  builders.eq_class_groups->AddIndex("index_on_eq_groups_url_index",
                                     {"facet_uri"});
  builders.eq_class_groups->AddIndex("index_on_eq_groups_set_id_index",
                                     {"set_id"});
  SealVersion(builders, /*expected_version=*/6u);

  builders.eq_class_members->AddColumn("change_password_url", "VARCHAR");
  builders.eq_class_groups->AddColumn("change_password_url", "VARCHAR");
  SealVersion(builders, /*expected_version=*/7u);
}

// Migrates from a given version or creates table depending if table exists or
// not.
bool EnsureCurrentVersion(sql::Database* db,
                          unsigned version,
                          SQLTableBuilder* builder) {
  if (db->DoesTableExist(builder->TableName())) {
    return builder->MigrateFrom(version, db);
  } else {
    return builder->CreateTable(db);
  }
}

}  // namespace

AffiliationDatabase::AffiliationDatabase() = default;

AffiliationDatabase::~AffiliationDatabase() = default;

bool AffiliationDatabase::Init(const base::FilePath& path) {
  sql_connection_ =
      std::make_unique<sql::Database>(sql::Database::Tag("Affiliation"));
  sql_connection_->set_error_callback(base::BindRepeating(
      &AffiliationDatabase::SQLErrorCallback, base::Unretained(this)));

  if (!sql_connection_->Open(path))
    return false;

  if (!sql_connection_->Execute("PRAGMA foreign_keys=1")) {
    sql_connection_->Poison();
    return false;
  }

  sql::MetaTable metatable;
  if (!metatable.Init(sql_connection_.get(), kVersion, kCompatibleVersion)) {
    sql_connection_->Poison();
    return false;
  }

  if (metatable.GetCompatibleVersionNumber() > kVersion) {
    LOG(WARNING) << "AffiliationDatabase is too new.";
    sql_connection_->Poison();
    return false;
  }

  SQLTableBuilder eq_classes_builder("eq_classes");
  SQLTableBuilder eq_class_members_builder("eq_class_members");
  SQLTableBuilder eq_class_groups_builder("eq_class_groups");
  SQLTableBuilder psl_extensions_builder("psl_extensions");
  SQLTableBuilders builders = {&eq_classes_builder, &eq_class_members_builder,
                               &eq_class_groups_builder,
                               &psl_extensions_builder};
  InitializeTableBuilders(builders);

  int version = metatable.GetVersionNumber();
  for (SQLTableBuilder* builder : builders.AsVector()) {
    if (!EnsureCurrentVersion(sql_connection_.get(), version, builder)) {
      LOG(WARNING) << "Failed to set up " << builder->TableName() << " table.";
      sql_connection_->Poison();
      return false;
    }
  }

  if (version < kVersion) {
    if (!metatable.SetVersionNumber(kVersion)) {
      return false;
    }
  }

  std::optional<int64_t> db_size = base::GetFileSize(path);
  if (db_size.has_value()) {
    base::UmaHistogramMemoryKB(
        "PasswordManager.AffiliationDatabase.DatabaseSize",
        db_size.value() / 1024);
  }

  return true;
}

bool AffiliationDatabase::GetAffiliationsAndBrandingForFacetURI(
    const FacetURI& facet_uri,
    AffiliatedFacetsWithUpdateTime* result) const {
  DCHECK(result);
  result->facets.clear();

  sql::Statement statement(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "SELECT m2.facet_uri, m2.facet_display_name, m2.facet_icon_url, "
      "m2.change_password_url,"
      "    c.last_update_time "
      "FROM eq_class_members m1, eq_class_members m2, eq_classes c "
      "WHERE m1.facet_uri = ? AND m1.set_id = m2.set_id AND m1.set_id = c.id"));
  statement.BindString(0, facet_uri.canonical_spec());

  while (statement.Step()) {
    result->facets.emplace_back(
        FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
        FacetBrandingInfo{
            statement.ColumnString(1),
            GURL(statement.ColumnStringView(2)),
        },
        GURL(statement.ColumnString(3)));
    result->last_update_time = statement.ColumnTime(4);
  }

  return !result->facets.empty();
}

void AffiliationDatabase::GetAllAffiliationsAndBranding(
    std::vector<AffiliatedFacetsWithUpdateTime>* results) const {
  DCHECK(results);
  results->clear();

  sql::Statement statement(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "SELECT m.facet_uri, m.facet_display_name, m.facet_icon_url, "
      "m.change_password_url,"
      "    c.last_update_time, c.id "
      "FROM eq_class_members m, eq_classes c "
      "WHERE m.set_id = c.id "
      "ORDER BY c.id"));

  int64_t last_eq_class_id = 0;
  while (statement.Step()) {
    int64_t eq_class_id = statement.ColumnInt64(5);
    if (results->empty() || eq_class_id != last_eq_class_id) {
      results->push_back(AffiliatedFacetsWithUpdateTime());
      last_eq_class_id = eq_class_id;
    }
    results->back().facets.emplace_back(
        FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
        FacetBrandingInfo{
            statement.ColumnString(1),
            GURL(statement.ColumnStringView(2)),
        },
        GURL(statement.ColumnString(3)));
    results->back().last_update_time =
        base::Time::FromInternalValue(statement.ColumnInt64(4));
  }
}

std::vector<GroupedFacets> AffiliationDatabase::GetAllGroups() const {
  std::vector<GroupedFacets> results;

  sql::Statement statement(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "SELECT g.facet_uri, g.main_domain, g.change_password_url, c.id, "
      "c.group_display_name, "
      "c.group_icon_url "
      "FROM eq_class_groups g, eq_classes c "
      "WHERE g.set_id = c.id "
      "ORDER BY c.id"));

  int64_t last_eq_class_id = 0;
  while (statement.Step()) {
    int64_t eq_class_id = statement.ColumnInt64(3);
    if (results.empty() || eq_class_id != last_eq_class_id) {
      GroupedFacets group;
      group.branding_info = FacetBrandingInfo{
          statement.ColumnString(4),
          GURL(statement.ColumnStringView(5)),
      };
      results.push_back(std::move(group));
      last_eq_class_id = eq_class_id;
    }
    results.back().facets.emplace_back(
        FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
        FacetBrandingInfo(), GURL(statement.ColumnString(2)),
        statement.ColumnString(1));
  }
  return results;
}

GroupedFacets AffiliationDatabase::GetGroup(const FacetURI& facet_uri) const {
  sql::Statement statement(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "SELECT m2.facet_uri, m2.main_domain, m2.change_password_url, "
      "c.group_display_name, "
      "c.group_icon_url, c.id "
      "FROM eq_class_groups m1, eq_class_groups m2, eq_classes c "
      "WHERE m1.facet_uri = ? AND m1.set_id = m2.set_id AND m1.set_id = c.id "
      "ORDER BY c.id"));
  statement.BindString(0, facet_uri.potentially_invalid_spec());

  GroupedFacets result;
  if (!statement.Step()) {
    // No such |facet_uri| in the database, return group with requested facet.
    result.facets.emplace_back(facet_uri);
    return result;
  }

  int64_t group_id = statement.ColumnInt64(5);

  // Add branding info for a group as it's the same for all steps.
  result.branding_info.name = statement.ColumnString(3);
  result.branding_info.icon_url = GURL(statement.ColumnStringView(4));

  result.facets.emplace_back(
      FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
      FacetBrandingInfo(), GURL(statement.ColumnString(2)),
      statement.ColumnString(1));

  while (statement.Step()) {
    // Return only the first group from the response, as other groups are exact
    // duplicates.
    if (group_id != statement.ColumnInt64(5)) {
      break;
    }
    result.facets.emplace_back(
        FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
        FacetBrandingInfo(), GURL(statement.ColumnString(2)),
        statement.ColumnString(1));
  }

  return result;
}

std::vector<std::string> AffiliationDatabase::GetPSLExtensions() const {
  std::vector<std::string> result;

  sql::Statement statement(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE, "SELECT domain FROM psl_extensions"));
  while (statement.Step()) {
    result.push_back(statement.ColumnString(0));
  }

  return result;
}

void AffiliationDatabase::DeleteAffiliationsAndBrandingForFacetURI(
    const FacetURI& facet_uri) {
  sql::Transaction transaction(sql_connection_.get());
  if (!transaction.Begin())
    return;

  sql::Statement statement_lookup(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "SELECT m.set_id FROM eq_class_members m "
      "WHERE m.facet_uri = ?"));
  statement_lookup.BindString(0, facet_uri.canonical_spec());

  // No such |facet_uri|, nothing to do.
  if (!statement_lookup.Step())
    return;

  int64_t eq_class_id = statement_lookup.ColumnInt64(0);

  // Children will get deleted due to 'ON DELETE CASCADE'.
  sql::Statement statement_parent(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE, "DELETE FROM eq_classes WHERE eq_classes.id = ?"));
  statement_parent.BindInt64(0, eq_class_id);
  if (!statement_parent.Run())
    return;

  transaction.Commit();
}

AffiliationDatabase::StoreAffiliationResult AffiliationDatabase::Store(
    const AffiliatedFacetsWithUpdateTime& affiliated_facets,
    const GroupedFacets& group) {
  DCHECK(!affiliated_facets.facets.empty());
  sql::Statement statement_parent(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "INSERT INTO eq_classes(last_update_time, group_display_name, "
      "group_icon_url) VALUES (?, ?, ?)"));

  sql::Statement statement_child(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "INSERT INTO "
      "eq_class_members(facet_uri, facet_display_name, facet_icon_url, "
      "change_password_url, set_id) "
      "VALUES (?, ?, ?, ?, ?)"));

  sql::Statement statement_groups(sql_connection_->GetCachedStatement(
      SQL_FROM_HERE,
      "INSERT INTO eq_class_groups(facet_uri, main_domain, "
      "change_password_url, set_id) "
      "VALUES (?, ?, ?, ?)"));

  sql::Transaction transaction(sql_connection_.get());
  if (!transaction.Begin()) {
    return StoreAffiliationResult::kFailedToStartTransaction;
  }

  statement_parent.BindTime(0, affiliated_facets.last_update_time);
  statement_parent.BindString(1, group.branding_info.name);
  statement_parent.BindString(
      2, group.branding_info.icon_url.possibly_invalid_spec());
  if (!statement_parent.Run()) {
    return StoreAffiliationResult::kFailedToAddSet;
  }

  int64_t eq_class_id = sql_connection_->GetLastInsertRowId();
  for (const Facet& facet : affiliated_facets.facets) {
    statement_child.Reset(true);
    statement_child.BindString(0, facet.uri.canonical_spec());
    statement_child.BindString(1, facet.branding_info.name);
    statement_child.BindString(
        2, facet.branding_info.icon_url.possibly_invalid_spec());
    statement_child.BindString(
        3, facet.change_password_url.possibly_invalid_spec());
    statement_child.BindInt64(4, eq_class_id);
    if (!statement_child.Run()) {
      return StoreAffiliationResult::kFailedToAddAffiliation;
    }
  }
  for (const Facet& facet : group.facets) {
    statement_groups.Reset(true);
    statement_groups.BindString(0, facet.uri.canonical_spec());
    statement_groups.BindString(1, facet.main_domain);
    statement_groups.BindString(
        2, facet.change_password_url.possibly_invalid_spec());
    statement_groups.BindInt64(3, eq_class_id);
    if (!statement_groups.Run()) {
      return StoreAffiliationResult::kFailedToAddGroup;
    }
  }

  if (!transaction.Commit()) {
    return StoreAffiliationResult::kFailedToCloseTransaction;
  }

  return StoreAffiliationResult::kSuccess;
}

void AffiliationDatabase::StoreAndRemoveConflicting(
    const AffiliatedFacetsWithUpdateTime& affiliation,
    const GroupedFacets& group,
    std::vector<AffiliatedFacetsWithUpdateTime>* removed_affiliations) {
  DCHECK(!affiliation.facets.empty());
  DCHECK(removed_affiliations);
  removed_affiliations->clear();

  sql::Transaction transaction(sql_connection_.get());
  if (!transaction.Begin())
    return;

  for (const Facet& facet : affiliation.facets) {
    AffiliatedFacetsWithUpdateTime old_affiliation;
    if (GetAffiliationsAndBrandingForFacetURI(facet.uri, &old_affiliation)) {
      if (!AreEquivalenceClassesEqual(old_affiliation.facets,
                                      affiliation.facets)) {
        removed_affiliations->push_back(old_affiliation);
      }
      DeleteAffiliationsAndBrandingForFacetURI(facet.uri);
    }
  }

  StoreAffiliationResult result = Store(affiliation, group);
  UMA_HISTOGRAM_ENUMERATION("PasswordManager.AffiliationDatabase.StoreResult",
                            result);

  transaction.Commit();
}

void AffiliationDatabase::RemoveMissingFacetURI(
    std::vector<FacetURI> facet_uris) {
  sql::Transaction transaction(sql_connection_.get());
  if (!transaction.Begin())
    return;

  auto current_facets = base::MakeFlatSet<std::string>(
      facet_uris, {}, &FacetURI::potentially_invalid_spec);

  std::set<int64_t> all_ids, found_ids;
  sql::Statement statement(sql_connection_->GetUniqueStatement(
      "SELECT m.facet_uri, m.set_id FROM eq_class_members m"));

  // For every facet in the database check if it exists in |current_facets|.
  while (statement.Step()) {
    std::string facet_uri = statement.ColumnString(0);
    int64_t eq_class_id = statement.ColumnInt64(1);

    all_ids.insert(eq_class_id);
    if (current_facets.contains(facet_uri)) {
      found_ids.insert(eq_class_id);
    }
  }

  // Remove any equivalence class which aren't represented in |current_facets|.
  for (const auto id : all_ids) {
    if (found_ids.find(id) == found_ids.end()) {
      sql::Statement statement_parent(sql_connection_->GetCachedStatement(
          SQL_FROM_HERE, "DELETE FROM eq_classes WHERE eq_classes.id = ?"));
      statement_parent.BindInt64(0, id);
      statement_parent.Run();
    }
  }

  transaction.Commit();
}

// static
void AffiliationDatabase::Delete(const base::FilePath& path) {
  bool success = sql::Database::Delete(path);
  DCHECK(success);
}

int AffiliationDatabase::GetDatabaseVersionForTesting() {
  sql::MetaTable metatable;
  // The second and third parameters to |MetaTable::Init| are ignored, given
  // that a metatable already exists. Hence they are not influencing the version
  // of the underlying database.
  DCHECK(sql::MetaTable::DoesTableExist(sql_connection_.get()));
  bool ok = metatable.Init(sql_connection_.get(), 1, 1);
  DCHECK(ok);
  return metatable.GetVersionNumber();
}

void AffiliationDatabase::UpdatePslExtensions(
    const std::vector<std::string>& domains) {
  DCHECK(!domains.empty());

  sql::Statement clear_table_statement(
      sql_connection_->GetUniqueStatement("DELETE FROM psl_extensions"));
  sql::Statement statement(sql_connection_->GetUniqueStatement(
      "INSERT INTO psl_extensions(domain) VALUES (?)"));
  sql::Transaction transaction(sql_connection_.get());

  if (!transaction.Begin()) {
    return;
  }

  // Clear all the records first.
  if (!clear_table_statement.Run()) {
    return;
  }

  for (const auto& domain : domains) {
    statement.Reset(true);
    statement.BindString(0, domain);
    if (!statement.Run()) {
      return;
    }
  }
  transaction.Commit();
}

void AffiliationDatabase::SQLErrorCallback(int error,
                                           sql::Statement* statement) {
  sql::UmaHistogramSqliteResult("PasswordManager.AffiliationDatabase.Error",
                                error);

  if (sql::IsErrorCatastrophic(error) && sql_connection_->is_open()) {
    // Normally this will poison the database, causing any subsequent operations
    // to silently fail without any side effects. However, if RazeAndPoison() is
    // called from the error callback in response to an error raised from within
    // sql::Database::Open, opening the now-razed database will be retried.
    sql_connection_->RazeAndPoison();
  }
}

}  // namespace affiliations
