// Copyright 2025 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/tab/tab_state_storage_database.h"

#include <memory>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/types/pass_key.h"
#include "chrome/browser/tab/payload_util.h"
#include "chrome/browser/tab/protocol/children.pb.h"
#include "chrome/browser/tab/protocol/token.pb.h"
#include "chrome/browser/tab/storage_id.h"
#include "chrome/browser/tab/storage_loaded_data.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "third_party/abseil-cpp//absl/container/flat_hash_map.h"

namespace tabs {
namespace {

using OpenTransaction = TabStateStorageDatabase::OpenTransaction;

// Update log:
// ??-08-2025, Version 1: Initial version of the database schema.
// 11-11-2025, Version 2: Add window_tag and is_off_the_record columns.
// 19-11-2025, Version 3: Change storage id type from int to blob and use token.
// 27-03-2025, Version 4: Add divergent nodes table.
const int kCurrentVersionNumber = 4;

// The last version of the database schema that is compatible with the current
// version. Any changes made to the database schema that would break
// compatibility with the current version should increment this number to
// trigger a raze and rebuild of the database schema. NOTE: once this database
// become the primary database for restore this number should NEVER be
// incremented and we should use a graceful upgrade path. We can however,
// consider inlining the upgrade path and incrementing this number if a
// significant enough time (O(years)) have passed that there is no longer a
// reasonable expectation that out-of-date users would be able to restore their
// session.
const int kCompatibleVersionNumber = 3;

static_assert(
    kCurrentVersionNumber >= kCompatibleVersionNumber,
    "Current version must be greater than or equal to compatible version.");

constexpr char kTabsTableName[] = "nodes";
constexpr char kDivergentNodesTableName[] = "divergent_nodes";

static constexpr char kCreateTabSchemaSql[] =
    "CREATE TABLE IF NOT EXISTS nodes("
    "id BLOB PRIMARY KEY NOT NULL,"
    "window_tag TEXT NOT NULL,"
    "is_off_the_record INTEGER NOT NULL,"
    "type INTEGER NOT NULL,"
    "children BLOB,"
    "payload BLOB)";

static constexpr char kCreateDivergentNodesSchemaSql[] =
    "CREATE TABLE IF NOT EXISTS divergent_nodes("
    "id BLOB PRIMARY KEY NOT NULL,"
    "window_tag TEXT NOT NULL,"
    "is_off_the_record INTEGER NOT NULL,"
    "children BLOB)";

static constexpr char kCreateIndexSql[] =
    "CREATE INDEX IF NOT EXISTS nodes_window_index "
    "ON nodes(window_tag, is_off_the_record)";

static constexpr char kCreateDivergentNodesIndexSql[] =
    "CREATE INDEX IF NOT EXISTS divergent_nodes_window_index "
    "ON divergent_nodes(window_tag, is_off_the_record)";

bool ExecuteSql(sql::Database* db, base::cstring_view sql_command) {
  DCHECK(db->IsSQLValid(sql_command)) << sql_command << " is not valid SQL.";
  return db->Execute(sql_command);
}

bool CreateSchema(sql::Database* db, sql::MetaTable* meta_table) {
  DCHECK(db->HasActiveTransactions());

  if (!ExecuteSql(db, kCreateTabSchemaSql)) {
    DLOG(ERROR) << "Failed to create tab schema.";
    return false;
  }

  if (!ExecuteSql(db, kCreateDivergentNodesSchemaSql)) {
    DLOG(ERROR) << "Failed to create divergent nodes schema.";
    return false;
  }

  if (!ExecuteSql(db, kCreateIndexSql)) {
    DLOG(ERROR) << "Failed to create index.";
    return false;
  }

  if (!ExecuteSql(db, kCreateDivergentNodesIndexSql)) {
    DLOG(ERROR) << "Failed to create divergent nodes index.";
    return false;
  }
  return true;
}

// TODO(crbug.com/459435876): Add histograms and enums to track database
// initialization failures.
bool InitSchema(sql::Database* db, sql::MetaTable* meta_table) {
  bool has_metatable = meta_table->DoesTableExist(db);
  bool has_either_tables = db->DoesTableExist(kTabsTableName) ||
                           db->DoesTableExist(kDivergentNodesTableName);
  if (!has_metatable && has_either_tables) {
    db->Raze();
  }

  if (sql::MetaTable::RazeIfIncompatible(db, kCompatibleVersionNumber,
                                         kCurrentVersionNumber) ==
      sql::RazeIfIncompatibleResult::kFailed) {
    LOG(ERROR) << "TabStateStorageDatabase failed to raze when an incompatible "
                  "version was detected.";
    return false;
  }

  sql::Transaction transaction(db);
  if (!transaction.Begin()) {
    DLOG(ERROR) << "Transaction could not be started.";
    return false;
  }

  if (!meta_table->Init(db, kCurrentVersionNumber, kCompatibleVersionNumber)) {
    LOG(ERROR) << "TabStateStorageDatabase failed to initialize meta table.";
    return false;
  }

  // Do not cache if the schema exists, it may have been razed earlier and needs
  // to be recreated.
  if (!db->DoesTableExist(kTabsTableName) && !CreateSchema(db, meta_table)) {
    LOG(ERROR) << "TabStateStorageDatabase failed to create schema.";
    return false;
  }

  // Any graceful upgrade logic when changing versions should go here in version
  // upgrade order.

  // Version 3 -> Version 4: Add divergent nodes table.
  if (meta_table->GetVersionNumber() == 3) {
    if (!ExecuteSql(db, kCreateDivergentNodesSchemaSql) ||
        !ExecuteSql(db, kCreateDivergentNodesIndexSql)) {
      LOG(ERROR)
          << "TabStateStorageDatabase failed to upgrade from version 3 to "
             "version 4.";
      return false;
    }
  }

  return meta_table->SetVersionNumber(kCurrentVersionNumber) &&
         meta_table->SetCompatibleVersionNumber(kCompatibleVersionNumber) &&
         transaction.Commit();
}

}  // namespace

OpenTransaction::OpenTransaction(sql::Database* db,
                                 base::PassKey<TabStateStorageDatabase>)
    : transaction_(db) {}

OpenTransaction::~OpenTransaction() = default;

bool OpenTransaction::HasFailed() {
  return mark_failed_;
}

void OpenTransaction::MarkFailed() {
  mark_failed_ = true;
}

sql::Transaction* OpenTransaction::GetTransaction(
    base::PassKey<TabStateStorageDatabase>) {
  return &transaction_;
}

void OpenTransaction::AddCallback(base::OnceClosure callback) {
  callbacks_.push_back(std::move(callback));
}

std::vector<base::OnceClosure> OpenTransaction::TakeCallbacks() {
  return std::move(callbacks_);
}

// static
bool TabStateStorageDatabase::OpenTransaction::IsValid(
    OpenTransaction* transaction) {
  return transaction && !transaction->HasFailed();
}

TabStateStorageDatabase::TabStateStorageDatabase(
    const base::FilePath& profile_path,
    bool support_off_the_record_data)
    : profile_path_(profile_path),
      support_off_the_record_data_(support_off_the_record_data),
      db_(sql::DatabaseOptions(), sql::Database::Tag("TabStateStorage")) {}

TabStateStorageDatabase::~TabStateStorageDatabase() = default;

bool TabStateStorageDatabase::Initialize() {
  base::FilePath db_dir = profile_path_.Append(FILE_PATH_LITERAL("Tabs"));
  if (!base::CreateDirectory(db_dir)) {
    LOG(ERROR) << "Failed to create directory for tab state storage database: "
               << db_dir;
    return false;
  }

  const base::FilePath db_path = db_dir.Append(FILE_PATH_LITERAL("TabDB"));
  if (!db_.Open(db_path)) {
    LOG(ERROR) << "Failed to open tab state storage database: "
               << db_.GetErrorMessage();
    return false;
  }

  if (!InitSchema(&db_, &meta_table_)) {
    DLOG(ERROR) << "Failed to create schema for tab state storage database: "
                << db_.GetErrorMessage();
    db_.Close();
    return false;
  }

  return true;
}

bool TabStateStorageDatabase::SaveNode(OpenTransaction* transaction,
                                       StorageId id,
                                       std::string_view window_tag,
                                       bool is_off_the_record,
                                       TabStorageType type,
                                       std::vector<uint8_t> payload,
                                       std::vector<uint8_t> children) {
  if (!support_off_the_record_data_ && is_off_the_record) {
    DLOG(ERROR) << "OTR saves are not supported by this database.";
    // Pretend we succeeded to avoid rollback.
    return true;
  }
  DCHECK(OpenTransaction::IsValid(transaction));

  if (is_off_the_record) {
    auto maybe_payload = Seal(id, window_tag, payload);
    if (!maybe_payload) {
      return false;
    }
    payload = std::move(*maybe_payload);
  }

  static constexpr char kInsertNodeSql[] =
      "INSERT OR REPLACE INTO nodes"
      "(id, window_tag, is_off_the_record, type, payload, children)"
      "VALUES (?,?,?,?,?,?)";

  DCHECK(db_.IsSQLValid(kInsertNodeSql));

  sql::Statement write_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kInsertNodeSql));

  write_statement.BindBlob(0, StorageIdToBlob(id));
  write_statement.BindString(1, window_tag);
  write_statement.BindInt(2, static_cast<int>(is_off_the_record));
  write_statement.BindInt(3, static_cast<int>(type));
  write_statement.BindBlob(4, std::move(payload));
  write_statement.BindBlob(5, std::move(children));

  return write_statement.Run();
}

bool TabStateStorageDatabase::SaveNodePayload(OpenTransaction* transaction,
                                              StorageId id,
                                              std::string_view window_tag,
                                              bool is_off_the_record,
                                              std::vector<uint8_t> payload) {
  DCHECK(OpenTransaction::IsValid(transaction));

  if (is_off_the_record) {
    auto maybe_payload = Seal(id, window_tag, payload);
    if (!maybe_payload) {
      return false;
    }
    payload = std::move(*maybe_payload);
  }

  static constexpr char kUpdatePayloadSql[] =
      "UPDATE nodes "
      "SET payload = ? "
      "WHERE id = ?";

  DCHECK(db_.IsSQLValid(kUpdatePayloadSql));

  sql::Statement write_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kUpdatePayloadSql));

  write_statement.BindBlob(0, std::move(payload));
  write_statement.BindBlob(1, StorageIdToBlob(id));

  return write_statement.Run();
}

bool TabStateStorageDatabase::SaveNodeChildren(OpenTransaction* transaction,
                                               StorageId id,
                                               std::vector<uint8_t> children) {
  DCHECK(OpenTransaction::IsValid(transaction));

  static constexpr char kUpdateChildrenSql[] =
      "UPDATE nodes "
      "SET children = ? "
      "WHERE id = ?";

  DCHECK(db_.IsSQLValid(kUpdateChildrenSql));

  sql::Statement write_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kUpdateChildrenSql));

  write_statement.BindBlob(0, std::move(children));
  write_statement.BindBlob(1, StorageIdToBlob(id));

  return write_statement.Run();
}

bool TabStateStorageDatabase::SaveDivergentNode(OpenTransaction* transaction,
                                                StorageId id,
                                                std::string_view window_tag,
                                                bool is_off_the_record,
                                                std::vector<uint8_t> children) {
  DCHECK(OpenTransaction::IsValid(transaction));

  static constexpr char kInsertDivergentNodeSql[] =
      "INSERT OR REPLACE INTO divergent_nodes"
      "(id, window_tag, is_off_the_record, children)"
      "VALUES (?,?,?,?)";

  DCHECK(db_.IsSQLValid(kInsertDivergentNodeSql));

  sql::Statement write_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kInsertDivergentNodeSql));

  write_statement.BindBlob(0, StorageIdToBlob(id));
  write_statement.BindString(1, window_tag);
  write_statement.BindInt(2, static_cast<int>(is_off_the_record));
  write_statement.BindBlob(3, std::move(children));

  return write_statement.Run();
}

bool TabStateStorageDatabase::RemoveNode(OpenTransaction* transaction,
                                         StorageId id) {
  DCHECK(OpenTransaction::IsValid(transaction));

  static constexpr char kDeleteNodeSql[] =
      "DELETE FROM nodes "
      "WHERE id = ?";

  DCHECK(db_.IsSQLValid(kDeleteNodeSql));

  sql::Statement write_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kDeleteNodeSql));

  write_statement.BindBlob(0, StorageIdToBlob(id));
  return write_statement.Run();
}

OpenTransaction* TabStateStorageDatabase::CreateTransaction() {
  if (!open_transaction_) {
    DCHECK_EQ(0, open_transaction_count_);
    open_transaction_.emplace(&db_, base::PassKey<TabStateStorageDatabase>());
    sql::Transaction* transaction_ptr =
        open_transaction_->GetTransaction(base::PassKey<TabStateStorageDatabase>());

    if (!transaction_ptr->Begin()) {
      DLOG(ERROR) << "Failed to begin transaction.";
      open_transaction_->MarkFailed();
    }
  }
  open_transaction_count_++;
  return &*open_transaction_;
}

bool TabStateStorageDatabase::CloseTransaction(
    OpenTransaction* open_transaction) {
  DCHECK(open_transaction_) << "There is no open transaction.";
  DCHECK_EQ(open_transaction, &*open_transaction_) << "Transaction mismatch.";
  DCHECK_GT(open_transaction_count_, 0);

  open_transaction_count_--;
  if (open_transaction_count_ > 0) {
    return true;
  }

  sql::Transaction* transaction = open_transaction->GetTransaction(
      base::PassKey<TabStateStorageDatabase>());

  bool success = false;
  if (open_transaction->HasFailed()) {
    transaction->Rollback();
    DLOG(ERROR) << "Transaction rolled back.";
  } else {
    success = transaction->Commit();
    if (!success) {
      DLOG(ERROR) << "Failed to commit transaction.";
      // TODO(crbug.com/454005648): If possible, record the reason for commit
      // failure here.
    }
  }

  std::vector<base::OnceClosure> callbacks_to_run;
  if (success) {
    callbacks_to_run = open_transaction->TakeCallbacks();
  }

  open_transaction_.reset();

  for (auto& callback : callbacks_to_run) {
    if (!callback.is_null()) {
      std::move(callback).Run();
    }
  }

  return success;
}

std::unique_ptr<StorageLoadedData> TabStateStorageDatabase::LoadAllNodes(
    std::string_view window_tag,
    bool is_off_the_record,
    std::unique_ptr<StorageLoadedData::Builder> builder) {
  // UNION ALL is used since it is more performant than a JOIN and avoids
  // matching IDs between the two tables.
  static constexpr char kSelectAllNodesSql[] =
      "SELECT id, type, payload, children, 0 as is_divergent FROM nodes "
      "WHERE window_tag = ? AND is_off_the_record = ? "
      "UNION ALL "
      "SELECT id, 0 as type, NULL as payload, children, 1 as is_divergent FROM "
      "divergent_nodes "
      "WHERE window_tag = ? AND is_off_the_record = ?";
  sql::Statement select_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kSelectAllNodesSql));
  select_statement.BindString(0, window_tag);
  select_statement.BindInt(1, static_cast<int>(is_off_the_record));
  select_statement.BindString(2, window_tag);
  select_statement.BindInt(3, static_cast<int>(is_off_the_record));
  while (select_statement.Step()) {
    StorageId id = StorageIdFromBlob(select_statement.ColumnBlob(0));
    TabStorageType type =
        static_cast<TabStorageType>(select_statement.ColumnInt(1));

    std::optional<base::span<const uint8_t>> children;
    if (type != TabStorageType::kTab) {
      children = select_statement.ColumnBlob(3);
    }
    const bool is_divergent = select_statement.ColumnBool(4);

    if (is_divergent) {
      builder->AddDivergentNode(id, type, children,
                                base::PassKey<TabStateStorageDatabase>());
      continue;
    }

    base::span<const uint8_t> payload = select_statement.ColumnBlob(2);

    if (is_off_the_record) {
      std::optional<std::vector<uint8_t>> open_payload =
          Open(id, window_tag, payload);
      if (!open_payload) {
        continue;
      }
      builder->AddNode(id, type, *open_payload, children,
                       base::PassKey<TabStateStorageDatabase>());
    } else {
      builder->AddNode(id, type, payload, children,
                       base::PassKey<TabStateStorageDatabase>());
    }
  }
  return builder->Build(base::PassKey<TabStateStorageDatabase>(), this);
}

int TabStateStorageDatabase::CountTabsForWindow(std::string_view window_tag,
                                                bool is_off_the_record) {
  static constexpr char kCountTabsForWindowSql[] =
      "SELECT COUNT(*) FROM nodes WHERE window_tag = ? AND is_off_the_record = "
      "? AND type = ?";
  sql::Statement count(
      db_.GetCachedStatement(SQL_FROM_HERE, kCountTabsForWindowSql));
  count.BindString(0, window_tag);
  count.BindInt(1, static_cast<int>(is_off_the_record));
  count.BindInt(2, static_cast<int>(TabStorageType::kTab));
  if (!count.Step()) {
    return 0;
  }
  return count.ColumnInt(0);
}

void TabStateStorageDatabase::ClearAllNodes() {
  static constexpr char kDeleteAllNodesSql[] = "DELETE FROM nodes";
  sql::Statement delete_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllNodesSql));
  delete_statement.Run();
}

void TabStateStorageDatabase::ClearAllDivergentNodes() {
  static constexpr char kDeleteAllDivergentNodesSql[] =
      "DELETE FROM divergent_nodes";
  sql::Statement delete_divergent_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllDivergentNodesSql));
  delete_divergent_statement.Run();
}

void TabStateStorageDatabase::ClearWindow(std::string_view window_tag) {
  static constexpr char kDeleteWindowSql[] =
      "DELETE FROM nodes WHERE window_tag = ?";
  sql::Statement delete_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kDeleteWindowSql));
  delete_statement.BindString(0, window_tag);
  delete_statement.Run();
}

void TabStateStorageDatabase::ClearDivergentNodesForWindow(
    std::string_view window_tag,
    bool is_off_the_record) {
  static constexpr char kDeleteDivergentNodesForWindowSql[] =
      "DELETE FROM divergent_nodes WHERE window_tag = ? AND "
      "is_off_the_record = ?";
  sql::Statement delete_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kDeleteDivergentNodesForWindowSql));
  delete_statement.BindString(0, window_tag);
  delete_statement.BindInt(1, static_cast<int>(is_off_the_record));
  delete_statement.Run();
}

void TabStateStorageDatabase::ClearDivergenceWindow(
    std::string_view window_tag) {
  static constexpr char kDeleteDivergenceWindowSql[] =
      "DELETE FROM divergent_nodes WHERE window_tag = ?";
  sql::Statement delete_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kDeleteDivergenceWindowSql));
  delete_statement.BindString(0, window_tag);
  delete_statement.Run();
}

bool TabStateStorageDatabase::ClearAllWindowsExcept(
    const std::vector<std::string>& window_tags) {
  DCHECK(!window_tags.empty());
  OpenTransaction* transaction = CreateTransaction();
  const std::string tag_placeholders = base::JoinString(
      std::vector<std::string_view>(window_tags.size(), "?"), ",");
  const std::string kDeleteSql = base::StrCat(
      {"DELETE FROM nodes WHERE window_tag NOT IN (", tag_placeholders, ")"});
  sql::Statement delete_statement(db_.GetUniqueStatement(kDeleteSql));
  for (size_t i = 0; i < window_tags.size(); i++) {
    delete_statement.BindString(i, window_tags[i]);
  }
  bool result1 = delete_statement.Run();

  const std::string kDeleteDivergentSql =
      base::StrCat({"DELETE FROM divergent_nodes WHERE window_tag NOT IN (",
                    tag_placeholders, ")"});
  sql::Statement delete_divergent_statement(
      db_.GetUniqueStatement(kDeleteDivergentSql));
  for (size_t i = 0; i < window_tags.size(); i++) {
    delete_divergent_statement.BindString(i, window_tags[i]);
  }
  bool result2 = delete_divergent_statement.Run();
  CloseTransaction(transaction);
  return result1 && result2;
}

bool TabStateStorageDatabase::ClearNodesForWindowExcept(
    std::string_view window_tag,
    bool is_off_the_record,
    const std::vector<StorageId>& ids) {
  const std::string id_placeholders =
      base::JoinString(std::vector<std::string_view>(ids.size(), "?"), ",");

  const std::string kDeleteNodesExceptSql =
      base::StrCat({"DELETE FROM nodes WHERE window_tag = ? AND "
                    "is_off_the_record = ? AND id NOT IN (",
                    id_placeholders, ")"});

  sql::Statement delete_statement(
      db_.GetUniqueStatement(kDeleteNodesExceptSql));
  delete_statement.BindString(0, window_tag);
  delete_statement.BindBool(1, is_off_the_record);

  for (size_t i = 0; i < ids.size(); i++) {
    delete_statement.BindBlob(i + 2, StorageIdToBlob(ids[i]));
  }
  return delete_statement.Run();
}

void TabStateStorageDatabase::SetKey(std::string window_tag,
                                     std::vector<uint8_t> key) {
  // TODO(crbug.com/462769977): Duplicate insertions seem to happen in tests
  // likely due to restarts that somehow don't trigger RemoveKey. This should be
  // investigated and fixed so this can be changed to a CHECK that insertion
  // is successful.
  keys_.insert_or_assign(std::move(window_tag), std::move(key));
}

void TabStateStorageDatabase::RemoveKey(std::string_view window_tag) {
  keys_.erase(window_tag);
}

std::optional<std::vector<uint8_t>> TabStateStorageDatabase::Seal(
    StorageId storage_id,
    std::string_view window_tag,
    base::span<const uint8_t> payload) {
  auto it = keys_.find(window_tag);
  if (it == keys_.end()) {
    LOG(WARNING) << "Failed to seal payload, no key found for window tag: "
                 << window_tag << " skipping save.";
    return std::nullopt;
  }
  return SealPayload(it->second, payload, storage_id);
}

std::optional<std::vector<uint8_t>> TabStateStorageDatabase::Open(
    StorageId storage_id,
    std::string_view window_tag,
    base::span<const uint8_t> payload) {
  auto it = keys_.find(window_tag);
  if (it == keys_.end()) {
    LOG(WARNING)
        << "Failed to open sealed payload, no key found for window tag: "
        << window_tag << " skipping restore.";
    return std::nullopt;
  }
  return OpenPayload(it->second, payload, storage_id);
}

#if defined(NDEBUG)
void TabStateStorageDatabase::PrintAll() {
  static constexpr char kSelectAllNodesSql[] =
      "SELECT id, window_tag, is_off_the_record, type, children FROM nodes";
  sql::Statement select_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kSelectAllNodesSql));

  int start_int = 0;
  absl::flat_hash_map<StorageId, int> storage_id_to_int;
  std::stringstream ss;

  ss << "Nodes Table Dump:\n";
  while (select_statement.Step()) {
    StorageId id = StorageIdFromBlob(select_statement.ColumnBlob(0));
    if (!storage_id_to_int.contains(id)) {
      start_int++;
      storage_id_to_int[id] = start_int;
    }

    std::string window_tag = select_statement.ColumnString(1);
    bool is_off_the_record = select_statement.ColumnInt(2);
    TabStorageType type =
        static_cast<TabStorageType>(select_statement.ColumnInt(3));

    base::span<const uint8_t> children_unparsed =
        select_statement.ColumnBlob(4);
    tabs_pb::Children children;
    children.ParseFromArray(children_unparsed.data(), children_unparsed.size());
    std::string children_str;

    if (children.storage_id_size() != 0) {
      for (int i = 0; i < children.storage_id_size(); i++) {
        tabs_pb::Token child = children.storage_id().at(i);
        StorageId child_id = StorageIdFromTokenProto(child);

        if (!storage_id_to_int.contains(child_id)) {
          start_int++;
          storage_id_to_int[child_id] = start_int;
        }

        children_str += base::NumberToString(storage_id_to_int[child_id]);
        if (i + 1 != children.storage_id_size()) {
          children_str += ", ";
        }
      }
      children_str = ", children=" + children_str;
    }

    ss << "Node: id=" << storage_id_to_int[id] << ", window_tag=" << window_tag
       << ", is_off_the_record=" << is_off_the_record
       << ", type=" << static_cast<int>(type) << children_str << "\n";
  }

  ss << "\nInt to Storage Id Map:\n";
  for (const auto& [storage_id, temp_int] : storage_id_to_int) {
    ss << "Entry: storage_id=" << storage_id.ToString()
       << ", temp_int=" << temp_int << "\n";
  }
  VLOG(1) << ss.str();
}
#endif

}  // namespace tabs
