// Copyright 2021 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/sessions/session_service_base.h"

#include <stddef.h>

#include <memory>
#include <set>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/to_string.h"
#include "base/task/sequenced_task_runner.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/web_contents_app_id_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_service_base_observer.h"
#include "chrome/browser/sessions/session_service_log.h"
#include "chrome/browser/sessions/session_service_utils.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/browser_init_state.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#include "chrome/browser/ui/startup/startup_browser_creator.h"
#include "chrome/browser/ui/tabs/features.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_selection_state.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/window_metadata/window_metadata_controller.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/sessions/content/content_serialized_navigation_builder.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/sessions/core/command_storage_manager.h"
#include "components/sessions/core/session_command.h"
#include "components/sessions/core/session_constants.h"
#include "components/sessions/core/session_id.h"
#include "components/sessions/core/session_service_commands.h"
#include "components/sessions/core/session_types.h"
#include "components/split_tabs/split_tab_id.h"
#include "components/split_tabs/split_tab_visual_data.h"
#include "components/tabs/public/split_tab_data.h"
#include "components/tabs/public/tab_group.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/session_storage_namespace.h"
#include "ui/base/mojom/window_show_state.mojom.h"

#if BUILDFLAG(IS_MAC)
#include "chrome/browser/app_controller_mac.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#include "ui/ozone/public/platform_session_manager.h"
#endif

using base::Time;
using content::NavigationEntry;
using content::WebContents;
using sessions::ContentSerializedNavigationBuilder;
using sessions::SerializedNavigationEntry;

namespace {

// Every kWritesPerReset commands triggers recreating the file.
const int kWritesPerReset = 250;

// User data key for WebContents to derive their types.
const void* const kSessionServiceBaseUserDataKey =
    &kSessionServiceBaseUserDataKey;

// User data key for BrowserContextData.
const void* const kProfileTaskRunnerKey = &kProfileTaskRunnerKey;
const void* const kProfileTaskRunnerKeyForApps = &kProfileTaskRunnerKeyForApps;

// Tracks the SequencedTaskRunner that SessionService uses for a particular
// profile. At certain points SessionService may be destroyed, and then
// recreated. This class ensures that when this happens, the same
// SequencedTaskRunner is used. Without this, each instance would have its
// own SequencedTaskRunner, which is problematic as it might then be possible
// for the newly created SessionService to attempt to read from a file the
// previous SessionService was still writing to. No two instances of
// SessionService for a particular profile and type combination can exist at the
// same time, but the backend (CommandStorageBackend) is destroyed on the
// SequencedTaskRunner, meaning without this, it might be possible for two
// CommandStorageBackends to exist at the same time and attempt to use the same
// file.
class TaskRunnerData : public base::SupportsUserData::Data {
 public:
  TaskRunnerData()
      : task_runner_(
            sessions::CommandStorageManager::CreateDefaultBackendTaskRunner()) {
  }
  TaskRunnerData(const TaskRunnerData&) = delete;
  TaskRunnerData& operator=(const TaskRunnerData&) = delete;
  ~TaskRunnerData() override = default;

  static scoped_refptr<base::SequencedTaskRunner>
  GetBackendTaskRunnerForProfile(Profile* profile,
                                 SessionServiceBase::SessionServiceType type) {
    const void* key =
        type == SessionServiceBase::SessionServiceType::kAppRestore
            ? kProfileTaskRunnerKeyForApps
            : kProfileTaskRunnerKey;

    TaskRunnerData* data =
        static_cast<TaskRunnerData*>(profile->GetUserData(key));
    if (!data) {
      profile->SetUserData(key, std::make_unique<TaskRunnerData>());
      data = static_cast<TaskRunnerData*>(profile->GetUserData(key));
    }
    return data->task_runner_;
  }

 private:
  scoped_refptr<base::SequencedTaskRunner> task_runner_;
};

// SessionServiceBaseUserData
// -------------------------------------------------------------
class SessionServiceBaseUserData : public base::SupportsUserData::Data {
 public:
  explicit SessionServiceBaseUserData(BrowserWindowInterface::Type type)
      : type_(type) {}
  ~SessionServiceBaseUserData() override = default;
  SessionServiceBaseUserData(const SessionServiceBaseUserData&) = delete;
  SessionServiceBaseUserData& operator=(const SessionServiceBaseUserData&) =
      delete;

  BrowserWindowInterface::Type type() const { return type_; }

 private:
  const BrowserWindowInterface::Type type_;
};

}  // namespace

// SessionServiceBase
// -------------------------------------------------------------
// Note: This is protected as it is only intended to be called by child classes
SessionServiceBase::SessionServiceBase(Profile* profile,
                                       SessionServiceType type)
    : profile_(profile) {
  // Covert SessionServiceType to backend CSM::SessionType enum.
  sessions::CommandStorageManager::SessionType backend_type =
      type == SessionServiceType::kSessionRestore
          ? sessions::CommandStorageManager::SessionType::kSessionRestore
          : sessions::CommandStorageManager::SessionType::kAppRestore;

  command_storage_manager_ = std::make_unique<sessions::CommandStorageManager>(
      backend_type, profile->GetPath(), this,
      g_browser_process->os_crypt_async(),
      TaskRunnerData::GetBackendTaskRunnerForProfile(profile, type));

  // We should never be created when incognito.
  DCHECK(!profile->IsOffTheRecord());
  if (profile->AllowsBrowserWindows()) {
    browser_collection_observer_.Observe(
        ProfileBrowserCollection::GetForProfile(profile));
  }
}

SessionServiceBase::~SessionServiceBase() {
  for (auto& observer : session_service_base_observers_) {
    observer.OnDestroying(this);
  }
  session_service_base_observers_.Clear();
  // command_storage_manager_->Save() should be called by child classes which
  // should have destructed the command_storage_manager.
  DCHECK(command_storage_manager_ == nullptr);
}

void SessionServiceBase::AddObserver(SessionServiceBaseObserver* observer) {
  session_service_base_observers_.AddObserver(observer);
}
void SessionServiceBase::RemoveObserver(SessionServiceBaseObserver* observer) {
  session_service_base_observers_.RemoveObserver(observer);
}

// static
BrowserWindowInterface::Type SessionServiceBase::GetBrowserTypeFromWebContents(
    content::WebContents* web_contents) {
  SessionServiceBaseUserData* data = static_cast<SessionServiceBaseUserData*>(
      web_contents->GetUserData(&kSessionServiceBaseUserDataKey));

  // Browser tab WebContents will have the UserData set on them. However, it is
  // possible that WebContents that are not tabs call into this code.
  // In that case, data will be null and we just return TYPE_NORMAL.
  if (!data) {
    return BrowserWindowInterface::Type::TYPE_NORMAL;
  }

  return data->type();
}

void SessionServiceBase::SetWindowVisibleOnAllWorkspaces(
    SessionID window_id,
    bool visible_on_all_workspaces) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateSetWindowVisibleOnAllWorkspacesCommand(
      window_id, visible_on_all_workspaces));
}

void SessionServiceBase::ResetFromCurrentBrowsers() {
  if (is_saving_enabled_) {
    ScheduleResetCommands();
  }
}

void SessionServiceBase::SetTabWindow(SessionID window_id, SessionID tab_id) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateSetTabWindowCommand(window_id, tab_id));
}

void SessionServiceBase::SetWindowBounds(
    SessionID window_id,
    const gfx::Rect& bounds,
    ui::mojom::WindowShowState show_state) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateSetWindowBoundsCommand(window_id, bounds, show_state));
}

void SessionServiceBase::SetWindowWorkspace(SessionID window_id,
                                            const std::string& workspace) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateSetWindowWorkspaceCommand(window_id, workspace));
}

void SessionServiceBase::SetTabIndexInWindow(SessionID window_id,
                                             SessionID tab_id,
                                             int new_index) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateSetTabIndexInWindowCommand(tab_id, new_index));
}

void SessionServiceBase::TabInserted(WebContents* contents) {
  sessions::SessionTabHelper* session_tab_helper =
      sessions::SessionTabHelper::FromWebContents(contents);
  if (!ShouldTrackChangesToWindow(session_tab_helper->window_id())) {
    return;
  }
  SetTabWindow(session_tab_helper->window_id(),
               session_tab_helper->session_id());
  std::string app_id = apps::GetAppIdForWebContents(contents);
  if (!app_id.empty()) {
    SetTabExtensionAppID(session_tab_helper->window_id(),
                         session_tab_helper->session_id(), app_id);
  }

  // Record the association between the SessionStorageNamespace and the
  // tab.
  //
  // TODO(ajwong): This should be processing the whole map rather than
  // just the default. This in particular will not work for tabs with only
  // isolated apps which won't have a default partition.
  content::SessionStorageNamespace* session_storage_namespace =
      contents->GetController().GetDefaultSessionStorageNamespace();
  ScheduleCommand(sessions::CreateSessionStorageAssociatedCommand(
      session_tab_helper->session_id(), session_storage_namespace->id()));
  session_storage_namespace->SetShouldPersist(true);

  // The tab being inserted is most likely already registered by
  // Browser::WebContentsCreated, but just register the UserData again.
  contents->SetUserData(&kSessionServiceBaseUserDataKey,
                        std::make_unique<SessionServiceBaseUserData>(
                            GetDesiredBrowserTypeForWebContents()));
}

void SessionServiceBase::TabClosing(WebContents* contents) {
  // Allow the associated sessionStorage to get deleted; it won't be needed
  // in the session restore.
  content::SessionStorageNamespace* session_storage_namespace =
      contents->GetController().GetDefaultSessionStorageNamespace();
  session_storage_namespace->SetShouldPersist(false);
  sessions::SessionTabHelper* session_tab_helper =
      sessions::SessionTabHelper::FromWebContents(contents);
  TabClosed(session_tab_helper->window_id(), session_tab_helper->session_id());
}

void SessionServiceBase::TabRestored(WebContents* tab, bool pinned) {
  if (!is_saving_enabled_) {
    return;
  }

  sessions::SessionTabHelper* session_tab_helper =
      sessions::SessionTabHelper::FromWebContents(tab);
  if (!ShouldTrackChangesToWindow(session_tab_helper->window_id())) {
    return;
  }

  BuildCommandsForTab(session_tab_helper->window_id(), tab, -1, std::nullopt,
                      std::nullopt, pinned, nullptr);
  command_storage_manager()->StartSaveTimer();
}

void SessionServiceBase::SetSelectedTabInWindow(SessionID window_id,
                                                int index) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  auto it = last_selected_tab_in_window_.find(window_id);
  if (it != last_selected_tab_in_window_.end() && it->second == index) {
    return;
  }
  last_selected_tab_in_window_[window_id] = index;

  ScheduleCommand(
      sessions::CreateSetSelectedTabInWindowCommand(window_id, index));
}

void SessionServiceBase::SetTabExtensionAppID(
    SessionID window_id,
    SessionID tab_id,
    const std::string& extension_app_id) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateSetTabExtensionAppIDCommand(tab_id, extension_app_id));
}

void SessionServiceBase::SetLastActiveTime(SessionID window_id,
                                           SessionID tab_id,
                                           base::Time last_active_time) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateLastActiveTimeCommand(tab_id, last_active_time));
}

void SessionServiceBase::GetLastSession(
    sessions::GetLastSessionCallback callback) {
  // OnGotSessionCommands maps the SessionCommands to browser state, then run
  // the callback.
  return command_storage_manager_->GetLastSessionCommands(
      base::BindOnce(&SessionServiceBase::OnGotSessionCommands,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void SessionServiceBase::SetWindowAppName(SessionID window_id,
                                          const std::string& app_name) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateSetWindowAppNameCommand(window_id, app_name));
}

void SessionServiceBase::SetPinnedState(SessionID window_id,
                                        SessionID tab_id,
                                        bool is_pinned) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreatePinnedStateCommand(tab_id, is_pinned));
}

bool SessionServiceBase::ShouldUseDelayedSave() {
  return should_use_delayed_save_;
}

void SessionServiceBase::OnWillSaveCommands() {
  if (!is_saving_enabled_) {
    // There should be no commands scheduled, otherwise data will be written,
    // potentially clobbering the last file.
    DCHECK(command_storage_manager_->pending_commands().empty());
    return;
  }

  RebuildCommandsIfRequired();
  did_save_commands_at_least_once_ |=
      !command_storage_manager()->pending_commands().empty();
}

void SessionServiceBase::OnErrorWritingSessionCommands() {
  LogSessionServiceWriteErrorEvent(profile_, false);
  rebuild_on_next_save_ = true;
  RebuildCommandsIfRequired();
}

void SessionServiceBase::SetTabUserAgentOverride(
    SessionID window_id,
    SessionID tab_id,
    const sessions::SerializedUserAgentOverride& user_agent_override) {
  // This is overridden by session_service implementation.
  // We still need it here because we derive from
  // sessions::SessionTabHelperDelegate.
  NOTREACHED();
}

void SessionServiceBase::SetSelectedNavigationIndex(SessionID window_id,
                                                    SessionID tab_id,
                                                    int index) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  auto it = tab_to_available_range_.find(tab_id);
  if (it != tab_to_available_range_.end()) {
    std::pair<int, int>& range = it->second;
    if (index < range.first || index > range.second) {
      // The new index is outside the range of what we've archived, schedule
      // a reset.
      ResetFromCurrentBrowsers();
      return;
    }
  }
  ScheduleCommand(
      sessions::CreateSetSelectedNavigationIndexCommand(tab_id, index));
}

void SessionServiceBase::UpdateTabNavigation(
    SessionID window_id,
    SessionID tab_id,
    const SerializedNavigationEntry& navigation) {
  if (!ShouldTrackURLForRestore(navigation.virtual_url()) ||
      !ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  auto it = tab_to_available_range_.find(tab_id);
  if (it != tab_to_available_range_.end()) {
    std::pair<int, int>& range = it->second;
    range.first = std::min(navigation.index(), range.first);
    range.second = std::max(navigation.index(), range.second);
  }
  ScheduleCommand(CreateUpdateTabNavigationCommand(tab_id, navigation));
}

void SessionServiceBase::TabNavigationPathPruned(SessionID window_id,
                                                 SessionID tab_id,
                                                 int index,
                                                 int count) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  DCHECK_GE(index, 0);
  DCHECK_GT(count, 0);

  // Update the range of available indices.
  auto it = tab_to_available_range_.find(tab_id);
  if (it != tab_to_available_range_.end()) {
    std::pair<int, int>& range = it->second;

    // if both range.first and range.second are also deleted.
    if (range.second >= index && range.second < index + count &&
        range.first >= index && range.first < index + count) {
      range.first = range.second = 0;
    } else {
      // Update range.first
      if (range.first >= index + count) {
        range.first = range.first - count;
      } else if (range.first >= index && range.first < index + count) {
        range.first = index;
      }

      // Update range.second
      if (range.second >= index + count) {
        range.second = std::max(range.first, range.second - count);
      } else if (range.second >= index && range.second < index + count) {
        range.second = std::max(range.first, index - 1);
      }
    }
  }

  return ScheduleCommand(
      sessions::CreateTabNavigationPathPrunedCommand(tab_id, index, count));
}

void SessionServiceBase::TabNavigationPathEntriesDeleted(SessionID window_id,
                                                         SessionID tab_id) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  // Multiple tabs might be affected by this deletion, so the rebuild is
  // delayed until next save.
  rebuild_on_next_save_ = true;
  command_storage_manager_->StartSaveTimer();
}

#if DCHECK_IS_ON()
base::Value SessionServiceBase::ToDebugValue() const {
  base::DictValue result;
  result.Set("profile", base::ToString(profile_.get()));
  if (command_storage_manager_) {
    result.Set("command_storage_manager",
               command_storage_manager_->ToDebugValue());
  }
  for (const auto& [id, range] : tab_to_available_range_) {
    base::DictValue* range_dict =
        result.EnsureDict("tab_to_available_range")
            ->EnsureDict(base::NumberToString(id.id()));
    range_dict->Set("first", range.first);
    range_dict->Set("second", range.first);
  }
  result.Set("rebuild_on_next_save", rebuild_on_next_save_);
  for (const auto& [id, tab_index] : last_selected_tab_in_window_) {
    result.EnsureDict("last_selected_tab_in_window")
        ->Set(base::NumberToString(id.id()), tab_index);
  }
  for (const SessionID& window_tracking : windows_tracking_) {
    result.EnsureList("windows_tracking")
        ->Append(base::NumberToString(window_tracking.id()));
  }
  result.Set("is_saving_enabled", is_saving_enabled_);
  result.Set("did_save_commands_at_least_once",
             did_save_commands_at_least_once_);
  return base::Value(std::move(result));
}
#endif  // DCHECK_IS_ON()

void SessionServiceBase::DestroyCommandStorageManager() {
  command_storage_manager_.reset(nullptr);
}

void SessionServiceBase::RemoveUnusedRestoreWindows(
    std::vector<std::unique_ptr<sessions::SessionWindow>>* window_list) {
  auto i = window_list->begin();
  while (i != window_list->end()) {
    sessions::SessionWindow* window = i->get();
    if (!ShouldRestoreWindowOfType(window->type)) {
      i = window_list->erase(i);
    } else {
      ++i;
    }
  }
}

void SessionServiceBase::OnBrowserActivated(BrowserWindowInterface* browser) {
  if (ShouldTrackBrowser(browser)) {
    ScheduleCommand(
        sessions::CreateSetActiveWindowCommand(browser->GetSessionID()));
  }
}

void SessionServiceBase::OnGotSessionCommands(
    sessions::GetLastSessionCallback callback,
    std::vector<std::unique_ptr<sessions::SessionCommand>> commands,
    bool read_error) {
  std::vector<std::unique_ptr<sessions::SessionWindow>> valid_windows;
  SessionID active_window_id = SessionID::InvalidValue();
  std::string platform_session_id;
  std::set<SessionID> discarded_window_ids;

  sessions::RestoreSessionFromCommands(commands, &valid_windows,
                                       &active_window_id, &platform_session_id,
                                       &discarded_window_ids);
  RemoveUnusedRestoreWindows(&valid_windows);

  InitializePlatformSessionIfNeeded(platform_session_id, discarded_window_ids);

  std::move(callback).Run(std::move(valid_windows), active_window_id,
                          read_error);
}

void SessionServiceBase::BuildCommandsForTab(
    SessionID window_id,
    WebContents* tab,
    int index_in_window,
    std::optional<tab_groups::TabGroupId> group,
    std::optional<split_tabs::SplitTabId> split,
    bool is_pinned,
    IdToRange* tab_to_available_range) {
  DCHECK(tab);
  DCHECK(window_id.is_valid());

  sessions::SessionTabHelper* session_tab_helper =
      sessions::SessionTabHelper::FromWebContents(tab);
  const SessionID session_id(session_tab_helper->session_id());
  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateSetTabWindowCommand(window_id, session_id));

  const int current_index = tab->GetController().GetCurrentEntryIndex();
  const int min_index =
      std::max(current_index - sessions::gMaxPersistNavigationCount, 0);
  const int max_index =
      std::min(current_index + sessions::gMaxPersistNavigationCount,
               tab->GetController().GetEntryCount());
  const int pending_index = tab->GetController().GetPendingEntryIndex();
  if (tab_to_available_range) {
    (*tab_to_available_range)[session_id] =
        std::pair<int, int>(min_index, max_index);
  }

  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateLastActiveTimeCommand(session_id,
                                            tab->GetLastActiveTime()));

  // TODO(stahon@microsoft.com) This might be movable to SessionService
  // when Chrome OS uses AppSessionService for app restores.
  // For now it needs to stay to support SessionService restoring apps.
  std::string app_id = apps::GetAppIdForWebContents(tab);
  if (!app_id.empty()) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateSetTabExtensionAppIDCommand(session_id, app_id));
  }

  for (int i = min_index; i < max_index; ++i) {
    NavigationEntry* entry = (i == pending_index)
                                 ? tab->GetController().GetPendingEntry()
                                 : tab->GetController().GetEntryAtIndex(i);
    DCHECK(entry);
    if (ShouldTrackURLForRestore(entry->GetVirtualURL()) &&
        !entry->IsInitialEntry()) {
      // Don't try to persist initial NavigationEntry, as it is not actually
      // associated with any navigation and will just result in about:blank on
      // session restore.
      const SerializedNavigationEntry navigation =
          ContentSerializedNavigationBuilder::FromNavigationEntry(i, entry);
      command_storage_manager()->AppendRebuildCommand(
          CreateUpdateTabNavigationCommand(session_id, navigation));
    }
  }
  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateSetSelectedNavigationIndexCommand(session_id,
                                                        current_index));

  if (index_in_window != -1) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateSetTabIndexInWindowCommand(session_id,
                                                   index_in_window));
  }

  if (is_pinned) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreatePinnedStateCommand(session_id, true));
  }

  // Record the association between the sessionStorage namespace and the tab.
  content::SessionStorageNamespace* session_storage_namespace =
      tab->GetController().GetDefaultSessionStorageNamespace();
  ScheduleCommand(sessions::CreateSessionStorageAssociatedCommand(
      session_tab_helper->session_id(), session_storage_namespace->id()));
}

void SessionServiceBase::BuildCommandsForBrowser(
    BrowserWindowInterface* browser,
    IdToRange* tab_to_available_range,
    std::set<SessionID>* windows_to_track) {
  DCHECK(is_saving_enabled_);
  DCHECK(browser);
  DCHECK(browser->GetSessionID().is_valid());

  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateSetWindowBoundsCommand(
          browser->GetSessionID(), browser->GetWindow()->GetRestoredBounds(),
          browser->GetWindow()->GetRestoredState()));

  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateSetWindowTypeCommand(
          browser->GetSessionID(),
          WindowTypeForBrowserType(browser->GetType())));

  if (!BrowserInitState::From(browser)->create_params().app_name.empty()) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateSetWindowAppNameCommand(
            browser->GetSessionID(),
            BrowserInitState::From(browser)->create_params().app_name));
  }

  if (!WindowMetadataController::From(browser)->user_title().empty()) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateSetWindowUserTitleCommand(
            browser->GetSessionID(),
            WindowMetadataController::From(browser)->user_title()));
  }

  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateSetWindowWorkspaceCommand(
          browser->GetSessionID(),
          BrowserWindow::FromBrowser(browser)->GetWorkspace()));

  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateSetWindowVisibleOnAllWorkspacesCommand(
          browser->GetSessionID(),
          BrowserWindow::FromBrowser(browser)->IsVisibleOnAllWorkspaces()));

  command_storage_manager()->AppendRebuildCommand(
      sessions::CreateSetSelectedTabInWindowCommand(
          browser->GetSessionID(),
          browser->GetTabStripModel()->active_index()));

  // Set the visual data for each tab group.
  TabStripModel* tab_strip = browser->GetTabStripModel();
  if (tab_strip->SupportsTabGroups()) {
    TabGroupModel* group_model = tab_strip->group_model();
    tab_groups::TabGroupSyncService* tab_group_service =
        tab_groups::TabGroupSyncServiceFactory::GetForProfile(
            browser->GetProfile());

    for (const tab_groups::TabGroupId& group_id :
         group_model->ListTabGroups()) {
      const tab_groups::TabGroupVisualData* visual_data =
          group_model->GetTabGroup(group_id)->visual_data();

      std::optional<std::string> saved_guid;
      if (tab_group_service) {
        const std::optional<tab_groups::SavedTabGroup> saved_group =
            tab_group_service->GetGroup(group_id);
        if (saved_group.has_value()) {
          saved_guid = saved_group->saved_guid().AsLowercaseString();
        }
      }

      command_storage_manager()->AppendRebuildCommand(
          sessions::CreateTabGroupMetadataUpdateCommand(group_id, visual_data,
                                                        std::move(saved_guid)));
    }
  }

  for (split_tabs::SplitTabId split_id : tab_strip->ListSplits()) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateSplitTabDataUpdateCommand(
            split_id, tab_strip->GetSplitData(split_id)->visual_data()));
  }

  if (base::FeatureList::IsEnabled(features::kTabGroupsFocusing) &&
      tab_strip->SupportsTabGroups()) {
    std::optional<tab_groups::TabGroupId> focused_group =
        tab_strip->GetFocusedGroup();
    if (focused_group.has_value()) {
      command_storage_manager()->AppendRebuildCommand(
          sessions::CreateAddWindowExtraDataCommand(
              browser->GetSessionID(),
              tabs::TabStripModelSelectionState::kFocusedTabGroupIdKey,
              focused_group->ToString()));
    }
  }

  int index = 0;
  for (const tabs::TabInterface* tab_interface : *tab_strip) {
    WebContents* tab = tab_interface->GetContents();
    DCHECK(tab);
    const std::optional<tab_groups::TabGroupId> group_id =
        tab_strip->GetTabGroupForTab(index);
    const std::optional<split_tabs::SplitTabId> split_id =
        tab_strip->GetSplitForTab(index);

    BuildCommandsForTab(browser->GetSessionID(), tab, index, group_id, split_id,
                        tab_interface->IsPinned(), tab_to_available_range);

    index++;
  }

  windows_to_track->insert(browser->GetSessionID());
}

void SessionServiceBase::BuildCommandsFromBrowsers(
    IdToRange* tab_to_available_range,
    std::set<SessionID>* windows_to_track) {
  DCHECK(is_saving_enabled_);
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [this, tab_to_available_range,
       windows_to_track](BrowserWindowInterface* browser) {
        // Make sure the browser has tabs and a window. When a browser is closed
        // the destructor is not necessarily run immediately. This means it's
        // possible for us to get a handle to a browser that is about to be
        // removed. If the tab count is 0 or the window is NULL, the browser is
        // about to be deleted, so we ignore it.
        if (ShouldTrackBrowser(browser) &&
            browser->GetTabStripModel()->count() && browser->GetWindow()) {
          BuildCommandsForBrowser(browser, tab_to_available_range,
                                  windows_to_track);
        }
        return true;
      });
}

void SessionServiceBase::ScheduleCommand(
    std::unique_ptr<sessions::SessionCommand> command) {
  if (!is_saving_enabled_) {
    return;
  }

  DCHECK(command);
  if (ReplacePendingCommand(command_storage_manager_.get(), &command)) {
    return;
  }

  bool is_closing_command = IsClosingCommand(command.get());
  command_storage_manager_->ScheduleCommand(std::move(command));
  // Don't schedule a reset on tab closed/window closed. Otherwise we may
  // lose tabs/windows we want to restore from if we exit right after this.
  if (!command_storage_manager_->pending_reset() &&
      command_storage_manager_->commands_since_reset() >= kWritesPerReset &&
      !is_closing_command) {
    ScheduleResetCommands();
  }
  DidScheduleCommand();
}

bool SessionServiceBase::ShouldTrackChangesToWindow(SessionID window_id) const {
  return windows_tracking_.contains(window_id);
}

bool SessionServiceBase::ShouldTrackBrowser(
    BrowserWindowInterface* browser) const {
  if (browser->GetProfile() != profile()) {
    return false;
  }

  if (BrowserInitState::From(browser)->omit_from_session_restore()) {
    return false;
  }

  // Never track app popup windows that do not have a trusted source (i.e.
  // popup windows spawned by an app). If this logic changes, be sure to also
  // change SessionRestoreImpl::CreateRestoredBrowser().
  if ((browser->GetType() == BrowserWindowInterface::TYPE_APP ||
       browser->GetType() == BrowserWindowInterface::TYPE_APP_POPUP) &&
      !WindowFeatureController::From(browser)->IsTrustedSource()) {
    return false;
  }

#if BUILDFLAG(IS_CHROMEOS)
  // Windows that are auto-started and prevented from closing are exempted from
  // tracking for session restore to prevent multiple unclosable open instances
  // of the same app.
  web_app::AppBrowserController* app_controller =
      web_app::AppBrowserController::From(browser);
  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(profile());
  // Checking for close prevention does not require an `AppLock` and
  // therefore `registrar_unsafe()` is safe to use.
  if (app_controller && provider &&
      provider->registrar_unsafe().IsPreventCloseEnabled(
          app_controller->app_id())) {
    return false;
  }
#endif  // #if BUILDFLAG(IS_CHROMEOS)

  return ShouldRestoreWindowOfType(
      WindowTypeForBrowserType(browser->GetType()));
}

sessions::CommandStorageManager*
SessionServiceBase::GetCommandStorageManagerForTest() {
  return command_storage_manager_.get();
}

void SessionServiceBase::SetAvailableRangeForTest(
    SessionID tab_id,
    const std::pair<int, int>& range) {
  tab_to_available_range_[tab_id] = range;
}

bool SessionServiceBase::GetAvailableRangeForTest(SessionID tab_id,
                                                  std::pair<int, int>* range) {
  auto i = tab_to_available_range_.find(tab_id);
  if (i == tab_to_available_range_.end()) {
    return false;
  }

  *range = i->second;
  return true;
}

void SessionServiceBase::SetSavingEnabled(bool enabled) {
  if (is_saving_enabled_ == enabled) {
    return;
  }
  is_saving_enabled_ = enabled;
  if (!is_saving_enabled_) {
    // Transitioning from enabled to disabled should happen very early on,
    // before any commands are actually written. If commands are written, then
    // the purpose of disabling will have failed (because by writing some
    // commands the previous session is going to be lost on exit).
    DCHECK(!did_save_commands_at_least_once_);
    command_storage_manager()->ClearPendingCommands();
  } else {
    ScheduleResetCommands();
  }
}

std::optional<std::string> SessionServiceBase::GetPlatformSessionId() {
  InitializePlatformSessionIfNeeded(/*restored_platform_session_id=*/{},
                                    /*discarded_window_ids=*/{});
  return platform_session_id_;
}

void SessionServiceBase::SetPlatformSessionIdForTesting(const std::string& id) {
  platform_session_id_ = id;
  ScheduleCommand(sessions::CreateSetPlatformSessionIdCommand(
      platform_session_id_.value()));
}

void SessionServiceBase::InitializePlatformSessionIfNeeded(
    const std::string& restored_platform_session_id,
    const std::set<SessionID>& discarded_window_ids) {
#if BUILDFLAG(IS_OZONE)
  ui::OzonePlatform* platform = ui::OzonePlatform::GetInstance();
  if (!platform->GetPlatformRuntimeProperties().supports_session_management ||
      platform_session_id_.has_value()) {
    return;
  }

  DCHECK(platform->GetSessionManager());
  ui::PlatformSessionManager* session_manager = platform->GetSessionManager();
  const bool should_restore = !restored_platform_session_id.empty();

  // TODO(crbug.com/352081012): Support post-crash restore reason.
  std::optional<std::string> actual_platform_session_id =
      should_restore ? session_manager->RestoreSession(
                           restored_platform_session_id,
                           ui::PlatformSessionManager::RestoreReason::kLaunch)
                     : platform->GetSessionManager()->CreateSession();

  if (actual_platform_session_id.has_value()) {
    DVLOG(1) << "Successfully initialized platform session."
             << " session_service=" << this
             << " session_id=" << actual_platform_session_id.value()
             << " discarded_windows=" << discarded_window_ids.size();
    platform_session_id_ = std::move(actual_platform_session_id);

    for (const SessionID& session_id : discarded_window_ids) {
      session_manager->RemoveWindow(platform_session_id_.value(),
                                    session_id.id());
    }

    ScheduleCommand(sessions::CreateSetPlatformSessionIdCommand(
        platform_session_id_.value()));
  }
#endif
}
