// Copyright 2012 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/sync_sessions/synced_session_tracker.h"

#include <algorithm>
#include <utility>

#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "components/sync/protocol/session_specifics.pb.h"
#include "components/sync/protocol/sync_enums.pb.h"
#include "components/sync_device_info/device_info_proto_enum_util.h"
#include "components/sync_sessions/features.h"
#include "components/sync_sessions/sync_sessions_client.h"

namespace sync_sessions {

namespace {

// Maximum time we allow a local tab stay unmapped (i.e. closed) but not freed
// due to data not having been committed yet. After that time, the data will
// be dropped.
constexpr base::TimeDelta kMaxUnmappedButUnsyncedLocalTabAge =
    base::Minutes(10);
// This is a generous cap to avoid issues with situations like sync being in
// error state (e.g. auth error) during which many tabs could be opened and
// closed, and still the information would not be committed.
constexpr int kMaxUnmappedButUnsyncedLocalTabCount = 20;

// Helper for iterating through all tabs within a window, and all navigations
// within a tab, to find if there's a valid syncable url.
bool ShouldSyncSessionWindow(SyncSessionsClient* sessions_client,
                             const sessions::SessionWindow& window) {
  for (const auto& tab : window.tabs) {
    for (const sessions::SerializedNavigationEntry& navigation :
         tab->navigations) {
      if (sessions_client->ShouldSyncURL(navigation.virtual_url())) {
        return true;
      }
    }
  }
  return false;
}

// Presentable means |foreign_session| must have syncable content.
bool IsPresentable(SyncSessionsClient* sessions_client,
                   const SyncedSession& foreign_session) {
  for (const auto& [window_id, window] : foreign_session.windows) {
    if (ShouldSyncSessionWindow(sessions_client, window->wrapped_window)) {
      return true;
    }
  }
  return false;
}

// Verify that tab and window IDs appear only once within a session. Intended to
// prevent http://crbug.com/360822 and crbug.com/803205.
bool IsValidSessionHeader(const sync_pb::SessionHeader& header) {
  std::set<int> session_window_ids;
  std::set<int> session_tab_ids;
  for (const sync_pb::SessionWindow& window : header.window()) {
    if (!session_window_ids.insert(window.window_id()).second) {
      return false;
    }

    for (int tab_id : window.tab()) {
      if (!session_tab_ids.insert(tab_id).second) {
        return false;
      }
    }
  }

  return true;
}

void PopulateSyncedSessionWindowFromSpecifics(
    const std::string& session_tag,
    const sync_pb::SessionWindow& specifics,
    base::Time mtime,
    SyncedSessionWindow* synced_session_window,
    SyncedSessionTracker* tracker) {
  sessions::SessionWindow* session_window =
      &synced_session_window->wrapped_window;

  // The session window must be initially empty (reset via
  // ResetSessionTracking()) to avoid leaving dangling pointers in
  // |synced_tab_map|.
  // TODO(crbug.com/41365570): replace with a DCHECK once PutTabInWindow() isn't
  // crashing anymore.
  CHECK(session_window->tabs.empty());

  if (specifics.has_window_id()) {
    session_window->window_id =
        SessionID::FromSerializedValue(specifics.window_id());
  }
  if (specifics.has_selected_tab_index()) {
    session_window->selected_tab_index = specifics.selected_tab_index();
  }
  synced_session_window->window_type = specifics.browser_type();
  if (specifics.has_browser_type()) {
    if (specifics.browser_type() ==
        sync_pb::SyncEnums_BrowserType_TYPE_TABBED) {
      session_window->type = sessions::SessionWindow::TYPE_NORMAL;
    } else {
      // Note: custom tabs are treated like popup windows on restore, as you can
      // restore a custom tab on a platform that doesn't support them.
      session_window->type = sessions::SessionWindow::TYPE_POPUP;
    }
  }

  session_window->timestamp = mtime;

  for (int i = 0; i < specifics.tab_size(); i++) {
    SessionID tab_id = SessionID::FromSerializedValue(specifics.tab(i));
    tracker->PutTabInWindow(session_tag, session_window->window_id, tab_id);
  }
}

void PopulateSyncedSessionFromSpecifics(
    const std::string& session_tag,
    const sync_pb::SessionHeader& header_specifics,
    base::Time mtime,
    SyncedSession* synced_session,
    SyncedSessionTracker* tracker) {
  if (header_specifics.has_client_name()) {
    synced_session->SetSessionName(header_specifics.client_name());
  }
  if (header_specifics.has_session_start_time_unix_epoch_millis()) {
    synced_session->SetStartTime(base::Time::FromMillisecondsSinceUnixEpoch(
        header_specifics.session_start_time_unix_epoch_millis()));
  }

  syncer::DeviceInfo::FormFactor device_form_factor =
      syncer::ToDeviceInfoFormFactor(header_specifics.device_form_factor());
  // Old clients only populate the device type, so the form factor needs to be
  // inferred.
  if (device_form_factor == syncer::DeviceInfo::FormFactor::kUnknown) {
    device_form_factor =
        syncer::DeriveFormFactorFromDeviceType(header_specifics.device_type());
  }
  if (device_form_factor != syncer::DeviceInfo::FormFactor::kUnknown) {
    synced_session->SetDeviceTypeAndFormFactor(
        syncer::ToDeviceInfoDeviceType(header_specifics.device_type()),
        device_form_factor);
  }
  synced_session->SetModifiedTime(
      std::max(mtime, synced_session->GetModifiedTime()));

  // Process all the windows and their tab information.
  int num_windows = header_specifics.window_size();
  DVLOG(1) << "Populating " << session_tag << " with " << num_windows
           << " windows.";

  for (int i = 0; i < num_windows; ++i) {
    const sync_pb::SessionWindow& window_s = header_specifics.window(i);
    SessionID window_id = SessionID::FromSerializedValue(window_s.window_id());
    bool success = tracker->PutWindowInSession(session_tag, window_id);
    // Window ID duplicates are filtered out in IsValidSessionHeader().
    DCHECK(success) << "Duplicate window ID in session";
    PopulateSyncedSessionWindowFromSpecifics(
        session_tag, window_s, synced_session->GetModifiedTime(),
        synced_session->windows[window_id].get(), tracker);
  }
}

}  // namespace

SyncedSessionTracker::TrackedSession::TrackedSession() = default;

SyncedSessionTracker::TrackedSession::~TrackedSession() = default;

SyncedSessionTracker::SyncedSessionTracker(SyncSessionsClient* sessions_client)
    : sessions_client_(sessions_client) {
  CHECK(sessions_client_);
}

SyncedSessionTracker::~SyncedSessionTracker() = default;

void SyncedSessionTracker::InitLocalSession(
    const std::string& local_session_tag,
    const std::string& local_session_name,
    syncer::DeviceInfo::DeviceType local_device_type,
    syncer::DeviceInfo::FormFactor local_device_form_factor) {
  DCHECK(local_session_tag_.empty());
  DCHECK(!local_session_tag.empty());
  local_session_tag_ = local_session_tag;

  SyncedSession* local_session = GetSession(local_session_tag);
  local_session->SetSessionName(local_session_name);
  local_session->SetDeviceTypeAndFormFactor(local_device_type,
                                            local_device_form_factor);
  local_session->SetSessionTag(local_session_tag);
  // Note: Do *not* call `SetStartTime()` here! `InitLocalSession()` gets called
  // on every browser startup (if sessions sync is enabled), but the session
  // start time should only be set when the session is initially created.
}

void SyncedSessionTracker::SetLocalSessionStartTime(
    base::Time local_session_start_time) {
  SyncedSession* local_session = GetSession(local_session_tag_);
  local_session->SetStartTime(local_session_start_time);
}

const std::string& SyncedSessionTracker::GetLocalSessionTag() const {
  return local_session_tag_;
}

std::vector<raw_ptr<const SyncedSession, VectorExperimental>>
SyncedSessionTracker::LookupAllSessions(SessionLookup lookup) const {
  return LookupSessions(lookup, /*exclude_local_session=*/false);
}

std::vector<raw_ptr<const SyncedSession, VectorExperimental>>
SyncedSessionTracker::LookupAllForeignSessions(SessionLookup lookup) const {
  return LookupSessions(lookup, /*exclude_local_session=*/true);
}

std::vector<const sessions::SessionWindow*>
SyncedSessionTracker::LookupSessionWindows(
    const std::string& session_tag) const {
  std::vector<const sessions::SessionWindow*> windows;

  const TrackedSession* session = LookupTrackedSession(session_tag);
  if (!session) {
    return windows;  // We have no record of this session.
  }

  for (const auto& [window_id, window] : session->synced_session.windows) {
    if (!window->wrapped_window.tabs.empty()) {
      windows.push_back(&window->wrapped_window);
    }
  }

  return windows;
}

const sessions::SessionTab* SyncedSessionTracker::LookupSessionTab(
    const std::string& tag,
    SessionID tab_id) const {
  if (!tab_id.is_valid()) {
    return nullptr;
  }

  const TrackedSession* session = LookupTrackedSession(tag);
  if (!session) {
    return nullptr;  // We have no record of this session.
  }

  auto tab_iter = session->synced_tab_map.find(tab_id);
  if (tab_iter == session->synced_tab_map.end()) {
    return nullptr;  // We have no record of this tab.
  }

  return tab_iter->second;
}

std::optional<sync_pb::SyncEnums::BrowserType>
SyncedSessionTracker::LookupWindowType(const std::string& session_tag,
                                       SessionID window_id) const {
  const TrackedSession* session = LookupTrackedSession(session_tag);
  if (!session) {
    return std::nullopt;
  }

  auto window_iter = session->synced_window_map.find(window_id);
  if (window_iter == session->synced_window_map.end()) {
    return std::nullopt;  // We have no record of this window.
  }

  return window_iter->second->window_type;
}

std::set<int> SyncedSessionTracker::LookupTabNodeIds(
    const std::string& session_tag) const {
  const TrackedSession* session = LookupTrackedSession(session_tag);
  return session ? session->tab_node_pool.GetAllTabNodeIds() : std::set<int>();
}


const SyncedSession* SyncedSessionTracker::LookupLocalSession() const {
  return LookupSession(local_session_tag_);
}

const SyncedSession* SyncedSessionTracker::LookupSession(
    const std::string& session_tag) const {
  const TrackedSession* session = LookupTrackedSession(session_tag);
  return session ? &session->synced_session : nullptr;
}

SyncedSession* SyncedSessionTracker::GetSession(
    const std::string& session_tag) {
  return &GetTrackedSession(session_tag)->synced_session;
}

void SyncedSessionTracker::DeleteForeignSession(
    const std::string& session_tag) {
  DCHECK_NE(local_session_tag_, session_tag);

  auto iter = session_map_.find(session_tag);
  if (iter == session_map_.end()) {
    return;
  }

  // SyncedSession's destructor will trigger deletion of windows which will in
  // turn trigger the deletion of tabs. This doesn't affect the convenience
  // maps.
  session_map_.erase(iter);
}

void SyncedSessionTracker::ResetSessionTracking(
    const std::string& session_tag) {
  TrackedSession* session = GetTrackedSession(session_tag);

  for (auto& [window_id, window] : session->synced_session.windows) {
    // First unmap the tabs in the window.
    for (auto& tab : window->wrapped_window.tabs) {
      SessionID tab_id = tab->tab_id;
      session->unmapped_tabs[tab_id] = std::move(tab);
    }
    window->wrapped_window.tabs.clear();

    // Then unmap the window itself.
    session->unmapped_windows[window_id] = std::move(window);
  }
  session->synced_session.windows.clear();
}

void SyncedSessionTracker::DeleteForeignTab(const std::string& session_tag,
                                            int tab_node_id) {
  DCHECK_NE(local_session_tag_, session_tag);
  TrackedSession* session = LookupTrackedSession(session_tag);
  if (session) {
    session->tab_node_pool.DeleteTabNode(tab_node_id);
  }
}

const SyncedSessionTracker::TrackedSession*
SyncedSessionTracker::LookupTrackedSession(
    const std::string& session_tag) const {
  auto it = session_map_.find(session_tag);
  return it == session_map_.end() ? nullptr : &it->second;
}

SyncedSessionTracker::TrackedSession*
SyncedSessionTracker::LookupTrackedSession(const std::string& session_tag) {
  auto it = session_map_.find(session_tag);
  return it == session_map_.end() ? nullptr : &it->second;
}

SyncedSessionTracker::TrackedSession* SyncedSessionTracker::GetTrackedSession(
    const std::string& session_tag) {
  TrackedSession* session = LookupTrackedSession(session_tag);
  if (session) {
    return session;
  }

  session = &session_map_[session_tag];
  DVLOG(1) << "Creating new session with tag " << session_tag << " at "
           << session;
  session->synced_session.SetSessionTag(session_tag);
  return session;
}

std::vector<raw_ptr<const SyncedSession, VectorExperimental>>
SyncedSessionTracker::LookupSessions(SessionLookup lookup,
                                     bool exclude_local_session) const {
  std::vector<raw_ptr<const SyncedSession, VectorExperimental>> sessions;
  for (const auto& [session_tag, tracked_session] : session_map_) {
    const SyncedSession& session = tracked_session.synced_session;
    if (lookup == PRESENTABLE && !IsPresentable(sessions_client_, session)) {
      continue;
    }
    // The comparison against |local_session_tag_| deals with the currently
    // active sync session (cache GUID or legacy session tag) but in addition
    // IsRecentLocalCacheGuid() is used to filter out older values of the
    // local cache GUID.
    if (exclude_local_session &&
        (session_tag == local_session_tag_ ||
         sessions_client_->IsRecentLocalCacheGuid(session_tag))) {
      continue;
    }
    sessions.push_back(&session);
  }
  return sessions;
}

void SyncedSessionTracker::CleanupSessionImpl(
    const std::string& session_tag,
    const base::RepeatingCallback<bool(int /*tab_node_id*/)>&
        is_tab_node_unsynced_cb) {
  TrackedSession* session = LookupTrackedSession(session_tag);
  if (!session) {
    return;
  }

  for (const auto& [window_id, window] : session->unmapped_windows) {
    session->synced_window_map.erase(window_id);
  }
  session->unmapped_windows.clear();

  int num_unmapped_and_unsynced = 0;
  auto tab_it = session->unmapped_tabs.begin();
  while (tab_it != session->unmapped_tabs.end()) {
    SessionID tab_id = tab_it->first;

    if (session_tag == local_session_tag_) {
      int tab_node_id = session->tab_node_pool.GetTabNodeIdFromTabId(tab_id);
      const base::TimeDelta time_since_last_modified =
          base::Time::Now() - tab_it->second->timestamp;

      if ((time_since_last_modified < kMaxUnmappedButUnsyncedLocalTabAge) &&
          num_unmapped_and_unsynced < kMaxUnmappedButUnsyncedLocalTabCount &&
          is_tab_node_unsynced_cb.Run(tab_node_id)) {
        // Our caller has decided that this tab node cannot be reused at this
        // point because there are pending changes to be committed that would
        // otherwise be lost). Hence, it stays unmapped but we do not free the
        // tab node for now (until future retries).
        ++tab_it;
        ++num_unmapped_and_unsynced;
        continue;
      }

      session->tab_node_pool.FreeTab(tab_id);
    }

    session->synced_tab_map.erase(tab_id);
    tab_it = session->unmapped_tabs.erase(tab_it);
  }
}

bool SyncedSessionTracker::IsTabUnmappedForTesting(SessionID tab_id) {
  const TrackedSession* session = LookupTrackedSession(local_session_tag_);
  if (!session) {
    return false;
  }

  return session->unmapped_tabs.count(tab_id) != 0;
}

bool SyncedSessionTracker::PutWindowInSession(const std::string& session_tag,
                                              SessionID window_id) {
  TrackedSession* session = GetTrackedSession(session_tag);
  if (session->synced_session.windows.count(window_id) != 0) {
    DVLOG(1) << "Window " << window_id << " already added to session "
             << session_tag;
    return false;
  }
  std::unique_ptr<SyncedSessionWindow> window;

  auto iter = session->unmapped_windows.find(window_id);
  if (iter != session->unmapped_windows.end()) {
    DCHECK_EQ(session->synced_window_map[window_id], iter->second.get());
    window = std::move(iter->second);
    session->unmapped_windows.erase(iter);
    DVLOG(1) << "Putting seen window " << window_id << " at " << window.get()
             << "in "
             << (session_tag == local_session_tag_ ? "local session"
                                                   : session_tag);
  } else {
    // Create the window.
    window = std::make_unique<SyncedSessionWindow>();
    window->wrapped_window.window_id = window_id;
    session->synced_window_map[window_id] = window.get();
    DVLOG(1) << "Putting new window " << window_id << " at " << window.get()
             << "in "
             << (session_tag == local_session_tag_ ? "local session"
                                                   : session_tag);
  }
  DCHECK_EQ(window->wrapped_window.window_id, window_id);
  DCHECK(GetSession(session_tag)->windows.end() ==
         GetSession(session_tag)->windows.find(window_id));
  GetSession(session_tag)->windows[window_id] = std::move(window);
  return true;
}

void SyncedSessionTracker::PutTabInWindow(const std::string& session_tag,
                                          SessionID window_id,
                                          SessionID tab_id) {
  TrackedSession* session = LookupTrackedSession(session_tag);
  DCHECK(session);

  // We're called here for two reasons. 1) We've received an update to the
  // SessionWindow information of a SessionHeader node for a session,
  // and 2) The SessionHeader node for our local session changed. In both cases
  // we need to update our tracking state to reflect the change.
  //
  // Because the SessionHeader nodes are separate from the individual tab nodes
  // and we don't store tab_node_ids in the header / SessionWindow specifics,
  // the tab_node_ids are not always available when processing headers. We know
  // that we will eventually process (via GetTab) every single tab node in the
  // system, so we permit ourselves to just call GetTab and ignore the result,
  // creating a placeholder SessionTab in the process.
  sessions::SessionTab* tab_ptr = GetTab(session_tag, tab_id);

  // The tab should be unmapped.
  std::unique_ptr<sessions::SessionTab> tab;
  auto it = session->unmapped_tabs.find(tab_id);
  if (it != session->unmapped_tabs.end()) {
    tab = std::move(it->second);
    session->unmapped_tabs.erase(it);
  } else {
    // The tab has already been mapped, possibly because of the tab node id
    // being reused across tabs. Find the existing tab and move it to the right
    // window.
    for (auto& [existing_window_id, existing_window] :
         GetSession(session_tag)->windows) {
      auto existing_tab_iter =
          std::ranges::find(existing_window->wrapped_window.tabs, tab_ptr,
                            &std::unique_ptr<sessions::SessionTab>::get);
      if (existing_tab_iter != existing_window->wrapped_window.tabs.end()) {
        tab = std::move(*existing_tab_iter);
        existing_window->wrapped_window.tabs.erase(existing_tab_iter);

        DVLOG(1) << "Moving tab " << tab_id << " from window "
                 << existing_window_id << " to " << window_id;
        break;
      }
    }
    // TODO(crbug.com/41365570): replace with a DCHECK once PutTabInWindow()
    // isn't crashing anymore.
    CHECK(tab) << " Unable to find tab " << tab_id
               << " within unmapped tabs or previously mapped windows."
               << " https://crbug.com/803205";
  }

  tab->window_id = window_id;
  DVLOG(1) << "  - tab " << tab_id << " added to window " << window_id;
  DCHECK(GetSession(session_tag)->windows.find(window_id) !=
         GetSession(session_tag)->windows.end());
  GetSession(session_tag)
      ->windows[window_id]
      ->wrapped_window.tabs.push_back(std::move(tab));
}

void SyncedSessionTracker::OnTabNodeSeen(const std::string& session_tag,
                                         int tab_node_id,
                                         SessionID tab_id) {
  // TODO(mastiz): Revisit if the exception for |local_session_tag_| can be
  // avoided and treat all sessions equally, which ideally involves merging this
  // function with ReassociateLocalTab().
  if (session_tag != local_session_tag_ &&
      tab_node_id != TabNodePool::kInvalidTabNodeID) {
    GetTrackedSession(session_tag)
        ->tab_node_pool.ReassociateTabNode(tab_node_id, tab_id);
  }
}

sessions::SessionTab* SyncedSessionTracker::GetTab(
    const std::string& session_tag,
    SessionID tab_id) {
  // TODO(crbug.com/41365570): replace with a DCHECK once PutTabInWindow() isn't
  // crashing anymore.
  CHECK(tab_id.is_valid());

  TrackedSession* session = GetTrackedSession(session_tag);
  sessions::SessionTab* tab_ptr = nullptr;
  auto iter = session->synced_tab_map.find(tab_id);
  if (iter != session->synced_tab_map.end()) {
    tab_ptr = iter->second;

    if (VLOG_IS_ON(1)) {
      std::string title;
      if (tab_ptr->navigations.size() > 0) {
        title =
            " (" + base::UTF16ToUTF8(tab_ptr->navigations.back().title()) + ")";
      }
      DVLOG(1) << "Getting "
               << (session_tag == local_session_tag_ ? "local session"
                                                     : session_tag)
               << "'s seen tab " << tab_id << " at " << tab_ptr << " " << title;
    }
  } else {
    auto tab = std::make_unique<sessions::SessionTab>();
    tab_ptr = tab.get();
    tab->tab_id = tab_id;
    session->synced_tab_map[tab_id] = tab_ptr;
    session->unmapped_tabs[tab_id] = std::move(tab);
    DVLOG(1) << "Getting "
             << (session_tag == local_session_tag_ ? "local session"
                                                   : session_tag)
             << "'s new tab " << tab_id << " at " << tab_ptr;
  }
  DCHECK(tab_ptr);
  DCHECK_EQ(tab_ptr->tab_id, tab_id);
  return tab_ptr;
}

void SyncedSessionTracker::CleanupSession(const std::string& session_tag) {
  DCHECK_NE(session_tag, local_session_tag_);
  // |is_tab_node_unsynced_cb| is only used for the local session, not needed
  // here.
  CleanupSessionImpl(
      session_tag,
      /*=is_tab_node_unsynced_cb=*/base::RepeatingCallback<bool(int)>());
}

std::set<int> SyncedSessionTracker::CleanupLocalTabs(
    const base::RepeatingCallback<bool(int /*tab_node_id*/)>&
        is_tab_node_unsynced_cb) {
  DCHECK(!local_session_tag_.empty());
  TrackedSession* session = GetTrackedSession(local_session_tag_);
  CleanupSessionImpl(local_session_tag_, is_tab_node_unsynced_cb);
  return session->tab_node_pool.CleanupFreeTabNodes();
}

int SyncedSessionTracker::LookupTabNodeFromTabId(const std::string& session_tag,
                                                 SessionID tab_id) const {
  const TrackedSession* session = LookupTrackedSession(session_tag);
  DCHECK(session);
  return session->tab_node_pool.GetTabNodeIdFromTabId(tab_id);
}

SessionID SyncedSessionTracker::LookupTabIdFromTabNodeId(
    const std::string& session_tag,
    int tab_node_id) const {
  const TrackedSession* session = LookupTrackedSession(session_tag);
  DCHECK(session);
  return session->tab_node_pool.GetTabIdFromTabNodeId(tab_node_id);
}

int SyncedSessionTracker::AssociateLocalTabWithFreeTabNode(SessionID tab_id) {
  DCHECK(!local_session_tag_.empty());
  TrackedSession* session = GetTrackedSession(local_session_tag_);

  // Ensure a placeholder SessionTab is in place, if not already. Although we
  // don't need a SessionTab to fulfill this request, this forces the creation
  // of one if it doesn't already exist. This helps to make sure we're tracking
  // this |tab_id| if TabNodePool is, and everyone's data structures are kept in
  // sync and as consistent as possible.
  GetTab(local_session_tag_, tab_id);  // Ignore result.

  return session->tab_node_pool.AssociateWithFreeTabNode(tab_id);
}

void SyncedSessionTracker::ReassociateLocalTab(int tab_node_id,
                                               SessionID new_tab_id) {
  DCHECK(!local_session_tag_.empty());
  DCHECK_NE(TabNodePool::kInvalidTabNodeID, tab_node_id);
  DCHECK(new_tab_id.is_valid());

  TrackedSession* session = LookupTrackedSession(local_session_tag_);
  DCHECK(session);

  SessionID old_tab_id =
      session->tab_node_pool.GetTabIdFromTabNodeId(tab_node_id);
  if (new_tab_id == old_tab_id) {
    return;
  }

  session->tab_node_pool.ReassociateTabNode(tab_node_id, new_tab_id);

  sessions::SessionTab* tab_ptr = nullptr;

  auto new_tab_iter = session->synced_tab_map.find(new_tab_id);
  auto old_tab_iter = session->synced_tab_map.find(old_tab_id);
  if (old_tab_id.is_valid() && old_tab_iter != session->synced_tab_map.end()) {
    tab_ptr = old_tab_iter->second;
    DCHECK(tab_ptr);

    // Remove the tab from the synced tab map under the old id.
    session->synced_tab_map.erase(old_tab_iter);

    if (new_tab_iter != session->synced_tab_map.end()) {
      // If both the old and the new tab already exist, delete the new tab
      // and use the old tab in its place.
      auto unmapped_tabs_iter = session->unmapped_tabs.find(new_tab_id);
      if (unmapped_tabs_iter != session->unmapped_tabs.end()) {
        session->unmapped_tabs.erase(unmapped_tabs_iter);
      } else {
        sessions::SessionTab* new_tab_ptr = new_tab_iter->second;
        for (auto& [existing_window_id, existing_window] :
             session->synced_session.windows) {
          auto& existing_window_tabs = existing_window->wrapped_window.tabs;
          auto tab_iter =
              std::ranges::find(existing_window_tabs, new_tab_ptr,
                                &std::unique_ptr<sessions::SessionTab>::get);
          if (tab_iter != existing_window_tabs.end()) {
            existing_window_tabs.erase(tab_iter);
            break;
          }
        }
      }

      session->synced_tab_map.erase(new_tab_iter);
    }

    // If the old tab is unmapped, update the tab id under which it is
    // indexed.
    auto unmapped_tabs_iter = session->unmapped_tabs.find(old_tab_id);
    if (old_tab_id.is_valid() &&
        unmapped_tabs_iter != session->unmapped_tabs.end()) {
      std::unique_ptr<sessions::SessionTab> tab =
          std::move(unmapped_tabs_iter->second);
      DCHECK_EQ(tab_ptr, tab.get());
      session->unmapped_tabs.erase(unmapped_tabs_iter);
      session->unmapped_tabs[new_tab_id] = std::move(tab);
    }
  }

  if (tab_ptr == nullptr) {
    // It's possible a placeholder is already in place for the new tab. If so,
    // reuse it, otherwise create a new one (which will default to unmapped).
    tab_ptr = GetTab(local_session_tag_, new_tab_id);
  }

  // Update the tab id.
  if (old_tab_id.is_valid()) {
    DVLOG(1) << "Remapped tab " << old_tab_id << " with node " << tab_node_id
             << " to tab " << new_tab_id;
  } else {
    DVLOG(1) << "Mapped new tab node " << tab_node_id << " to tab "
             << new_tab_id;
  }
  tab_ptr->tab_id = new_tab_id;

  // Add the tab back into the tab map with the new id.
  session->synced_tab_map[new_tab_id] = tab_ptr;
}



void SyncedSessionTracker::Clear() {
  session_map_.clear();
  local_session_tag_.clear();
}

void SyncedSessionTracker::TryUpdateSessionNameFromDeviceInfo(
    const std::string& session_tag) {
  CHECK(sessions_client_);
  CHECK(LookupSession(session_tag));

  std::optional<std::string> preferred_name =
      sessions_client_->GetSessionDisplayNameFromDeviceInfo(session_tag);
  if (preferred_name.has_value()) {
    GetSession(session_tag)->SetSessionName(*preferred_name);
  }
}

void UpdateTrackerWithSpecifics(const sync_pb::SessionSpecifics& specifics,
                                base::Time modification_time,
                                SyncedSessionTracker* tracker) {
  std::string session_tag = specifics.session_tag();
  SyncedSession* session = tracker->GetSession(session_tag);
  if (specifics.has_header()) {
    // Read in the header data for this session. Header data is
    // essentially a collection of windows, each of which has an ordered id list
    // for their tabs.

    if (!IsValidSessionHeader(specifics.header())) {
      DLOG(WARNING) << "Ignoring session node with invalid header "
                    << "and tag " << session_tag << ".";
      return;
    }

    // Load (or create) the SyncedSession object for this client.
    const sync_pb::SessionHeader& header = specifics.header();

    // Reset the tab/window tracking for this session (must do this before
    // we start calling PutWindowInSession and PutTabInWindow so that all
    // unused tabs/windows get cleared by the CleanupSession(...) call).
    tracker->ResetSessionTracking(session_tag);

    PopulateSyncedSessionFromSpecifics(session_tag, header, modification_time,
                                       session, tracker);
    if (base::FeatureList::IsEnabled(kSyncSessionsUsePreferredDisplayName)) {
      tracker->TryUpdateSessionNameFromDeviceInfo(session_tag);
    }

    // Delete any closed windows and unused tabs as necessary. We exclude the
    // local session here because it should be cleaned up explicitly with
    // CleanupLocalTabs().
    if (session_tag != tracker->GetLocalSessionTag()) {
      tracker->CleanupSession(session_tag);
    }
  } else if (specifics.has_tab()) {
    const sync_pb::SessionTab& tab_s = specifics.tab();
    SessionID tab_id = SessionID::FromSerializedValue(tab_s.tab_id());
    if (!tab_id.is_valid()) {
      DLOG(WARNING) << "Ignoring session tab with invalid tab ID for session "
                    << "tag " << session_tag << " and node ID "
                    << specifics.tab_node_id() << ".";
      return;
    }

    DVLOG(1) << "Populating " << session_tag << "'s tab id " << tab_id
             << " from node " << specifics.tab_node_id();

    // Ensure the tracker is aware of the tab node id. Deleting foreign sessions
    // requires deleting all relevant tab nodes, and it's easier to track the
    // tab node ids themselves separately from the tab ids.
    //
    // Note that TabIDs are not stable across restarts of a client. Consider
    // this example with two tabs:
    //
    // http://a.com  TabID1 --> NodeIDA
    // http://b.com  TabID2 --> NodeIDB
    //
    // After restart, tab ids are reallocated. e.g, one possibility:
    // http://a.com TabID2 --> NodeIDA
    // http://b.com TabID1 --> NodeIDB
    //
    // If that happened on a remote client, here we will see an update to
    // TabID1 with tab_node_id changing from NodeIDA to NodeIDB, and TabID2
    // with tab_node_id changing from NodeIDB to NodeIDA.
    //
    // We can also wind up here if we created this tab as an out-of-order
    // update to the header node for this session before actually associating
    // the tab itself, so the tab node id wasn't available at the time and
    // is currently kInvalidTabNodeID.
    //
    // In both cases, we can safely throw it into the set of node ids.
    tracker->OnTabNodeSeen(session_tag, specifics.tab_node_id(), tab_id);
    sessions::SessionTab* tab = tracker->GetTab(session_tag, tab_id);
    if (!tab->timestamp.is_null() && tab->timestamp > modification_time) {
      DVLOG(1) << "Ignoring " << session_tag << "'s session tab " << tab_id
               << " with earlier modification time: " << tab->timestamp
               << " vs " << modification_time;
      return;
    }

    // Update SessionTab based on protobuf.
    SetSessionTabFromSyncData(tab_s, modification_time, tab);

    // Update the last modified time.
    if (session->GetModifiedTime() < modification_time) {
      session->SetModifiedTime(modification_time);
    }
  } else {
    LOG(WARNING)
        << "Ignoring session node with missing header/tab fields and tag "
        << session_tag << ".";
  }
}

void SerializeTrackerToSpecifics(
    const SyncedSessionTracker& tracker,
    const base::RepeatingCallback<void(const std::string& session_name,
                                       sync_pb::SessionSpecifics* specifics)>&
        output_cb) {
  std::map<std::string, std::set<int>> session_tag_to_tab_node_ids;
  for (const SyncedSession* session :
       tracker.LookupAllSessions(SyncedSessionTracker::RAW)) {
    const std::string& session_tag = session->GetSessionTag();
    // Request all tabs (associated or free).
    session_tag_to_tab_node_ids[session_tag] =
        tracker.LookupTabNodeIds(session_tag);
    // Request the header too.
    session_tag_to_tab_node_ids[session_tag].insert(
        TabNodePool::kInvalidTabNodeID);
  }
  SerializePartialTrackerToSpecifics(tracker, session_tag_to_tab_node_ids,
                                     output_cb);
}

void SerializePartialTrackerToSpecifics(
    const SyncedSessionTracker& tracker,
    const std::map<std::string, std::set<int>>& session_tag_to_tab_node_ids,
    const base::RepeatingCallback<void(const std::string& session_name,
                                       sync_pb::SessionSpecifics* specifics)>&
        output_cb) {
  // Process tabs and header.
  for (const auto& [session_tag, node_ids] : session_tag_to_tab_node_ids) {
    const SyncedSession* session = tracker.LookupSession(session_tag);
    if (!session) {
      // Unknown session.
      continue;
    }

    const std::set<int> known_tab_node_ids =
        tracker.LookupTabNodeIds(session_tag);

    for (int tab_node_id : node_ids) {
      // Header entity.
      if (tab_node_id == TabNodePool::kInvalidTabNodeID) {
        sync_pb::SessionSpecifics header_pb;
        header_pb.set_session_tag(session_tag);
        session->ToSessionHeaderProto().Swap(header_pb.mutable_header());
        output_cb.Run(session->GetSessionName(), &header_pb);
        continue;
      }

      // Check if |tab_node_id| is known by the tracker.
      if (known_tab_node_ids.count(tab_node_id) == 0) {
        continue;
      }

      // Tab entities.
      const SessionID tab_id =
          tracker.LookupTabIdFromTabNodeId(session_tag, tab_node_id);
      if (!tab_id.is_valid()) {
        // This can be the case for tabs that were unmapped because we received
        // a new foreign tab with the same tab ID (the last one wins), so we
        // don't remember the tab ID for the original |tab_node_id|. Instead of
        // returning partially populated SessionSpecifics (without tab ID), we
        // simply drop them, because older clients don't handle well such
        // invalid specifics.
        continue;
      }

      const sessions::SessionTab* tab =
          tracker.LookupSessionTab(session_tag, tab_id);
      if (tab) {
        // Associated/mapped tab node.
        sync_pb::SessionSpecifics tab_pb;
        tab_pb.set_session_tag(session_tag);
        tab_pb.set_tab_node_id(tab_node_id);
        *tab_pb.mutable_tab() = SessionTabToSyncData(
            *tab, tracker.LookupWindowType(session_tag, tab->window_id));
        output_cb.Run(session->GetSessionName(), &tab_pb);
        continue;
      }

      // Create entities for unmapped tabs nodes.
      sync_pb::SessionSpecifics tab_pb;
      tab_pb.set_tab_node_id(tab_node_id);
      tab_pb.set_session_tag(session_tag);
      tab_pb.mutable_tab()->set_tab_id(tab_id.id());
      output_cb.Run(session->GetSessionName(), &tab_pb);
    }
  }
}

}  // namespace sync_sessions
