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

#import "ios/chrome/app/profile/profile_controller.h"

#import <algorithm>
#import <memory>
#import <utility>

#import "base/critical_closure.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/functional/concurrent_closures.h"
#import "base/memory/scoped_refptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/task_traits.h"
#import "base/task/thread_pool.h"
#import "base/time/time.h"
#import "components/content_settings/core/browser/host_content_settings_map.h"
#import "components/content_settings/core/common/content_settings.h"
#import "components/content_settings/core/common/content_settings_types.h"
#import "components/desktop_to_mobile_promos/features.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/language/core/browser/language_usage_metrics.h"
#import "components/language/core/browser/pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/translate/core/browser/translate_metrics_logger_impl.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/metrics_mediator.h"
#import "ios/chrome/app/deferred_initialization_runner.h"
#import "ios/chrome/app/deferred_initialization_task_names.h"
#import "ios/chrome/app/profile/app_icon_launched_profile_agent.h"
#import "ios/chrome/app/profile/application_storage_metrics.h"
#import "ios/chrome/app/profile/certificate_policy_profile_agent.h"
#import "ios/chrome/app/profile/features.h"
#import "ios/chrome/app/profile/first_run_profile_agent.h"
#import "ios/chrome/app/profile/identity_confirmation_profile_agent.h"
#import "ios/chrome/app/profile/multi_profile_forced_migration_profile_agent.h"
#import "ios/chrome/app/profile/otr_profile_destroyer_profile_agent.h"
#import "ios/chrome/app/profile/post_restore_profile_agent.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/app/profile/profile_state_observer.h"
#import "ios/chrome/app/profile/search_engine_choice_profile_agent.h"
#import "ios/chrome/app/profile/session_metrics_profile_agent.h"
#import "ios/chrome/app/profile/synced_set_up_profile_agent.h"
#import "ios/chrome/app/profile/welcome_back_screen_profile_agent.h"
#import "ios/chrome/app/spotlight/spotlight_manager.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/browser/backend_promo/model/backend_promo_profile_agent.h"
#import "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h"
#import "ios/chrome/browser/credential_provider/model/credential_provider_buildflags.h"
#import "ios/chrome/browser/cross_platform_promos/model/cross_platform_promos_service.h"
#import "ios/chrome/browser/cross_platform_promos/model/cross_platform_promos_service_factory.h"
#import "ios/chrome/browser/enterprise/model/idle/idle_service.h"
#import "ios/chrome/browser/enterprise/model/idle/idle_service_factory.h"
#import "ios/chrome/browser/external_files/model/external_file_remover.h"
#import "ios/chrome/browser/external_files/model/external_file_remover_factory.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/first_run/public/features.h"
#import "ios/chrome/browser/mailto_handler/model/mailto_handler_service_factory.h"
#import "ios/chrome/browser/ntp/model/home_background_customization_promo_profile_agent.h"
#import "ios/chrome/browser/profile_metrics/model/profile_activity_profile_agent.h"
#import "ios/chrome/browser/search_engines/model/extension_search_engine_data_updater.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state_animator.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state_observer.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/features.h"
#import "ios/chrome/browser/shared/model/profile/profile_attributes_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_attributes_storage_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.h"
#import "ios/chrome/browser/shared/model/profile/scoped_profile_keep_alive_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/snapshots/model/constants.h"
#import "ios/chrome/browser/translate/model/chrome_ios_translate_client.h"
#import "ios/chrome/browser/web_state_list/model/web_usage_enabler/web_usage_enabler_browser_agent.h"
#import "ios/chrome/browser/welcome_back/model/features.h"
#import "ios/components/cookie_util/cookie_util.h"
#import "ios/public/provider/chrome/browser/raccoon/raccoon_api.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "net/cookies/cookie_store.h"
#import "net/url_request/url_request_context.h"
#import "net/url_request/url_request_context_getter.h"
#import "ui/base/device_form_factor.h"

#if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)
#import "ios/chrome/browser/credential_provider/model/credential_provider_service_factory.h"  // nogncheck
#import "ios/chrome/browser/credential_provider/model/credential_provider_util.h"  // nogncheck
#import "ios/chrome/browser/passwords/model/password_manager_util_ios.h"  // nogncheck
#import "ios/chrome/browser/sync/model/sync_service_factory.h"  // nogncheck
#endif

namespace {

using SessionIds = ProfileAttributesIOS::SessionIds;

// The delay for cleaning external files.
constexpr base::TimeDelta kExternalFilesCleanupDelay = base::Minutes(1);

// Name of the block deleting the leftover session state files.
NSString* const kStartupPurgeUnassociatedData = @"StartupPurgeUnassociatedData";

// Name of the block creating the MailtoHandlerService instance.
NSString* const kStartupCreateMailtoHandlerService =
    @"StartupCreateMailtoHandlerService";

// Name of the block cleaning up the offline reading list directory.
NSString* const kStartupCleanupReadingListOfflineData =
    @"StartupCleanupReadingListOfflineData";

// Name of the block that resynchronize the Spotlight index.
NSString* const kStartResyncSpotlightIndex = @"StartResyncSpotlightIndex";

#if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)
// Name of the block cleaning up the favicons.
NSString* const kStartupCleanupFavicons = @"StartupCleanupFavicons";
#endif

#if !TARGET_OS_SIMULATOR
// Name of the block logging the storage metrics.
NSString* const kStartupLogStorageMetrics = @"StartupLogStorageMetrics";

// The minimum amount of time between calculating and logging metrics about
// the amount of device storage space used by Chrome.
constexpr base::TimeDelta kMinimumTimeBetweenDocumentsSizeLogging =
    base::Days(14);

// Returns whether the storage metrics should be logged.
bool ShouldLogStorageMetrics(PrefService* pref_service) {
  const base::Time last_logged =
      pref_service->GetTime(prefs::kLastApplicationStorageMetricsLogTime);

  return base::Time::Now() - last_logged >=
         kMinimumTimeBetweenDocumentsSizeLogging;
}
#endif

// Flushes the CookieStore on the IO thread and invokes `closure` on completion
// on an unspecified sequence.
void FlushCookieStoreOnIOThread(
    scoped_refptr<net::URLRequestContextGetter> getter,
    base::OnceClosure closure) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  getter->GetURLRequestContext()->cookie_store()->FlushStore(
      std::move(closure));
}

// Purges data for discarded sessions `session_ids` relative to profile's
// storage paths (regulard and off-the-record).
void PurgeDataForSessions(const SessionIds& session_ids,
                          const std::array<base::FilePath, 2>& storage_paths) {
  const std::array<base::FilePath::StringViewType, 2> directories = {
      kSessionRestorationDirname,
      kSnapshotsDirName,
  };

  for (const base::FilePath& storage_path : storage_paths) {
    for (const std::string_view directory : directories) {
      const base::FilePath sub_directory = storage_path.Append(directory);
      for (const std::string& session : session_ids) {
        const base::FilePath path = sub_directory.Append(session);
        std::ignore = base::DeletePathRecursively(path);
      }
    }
  }
}

// Removes `session_ids` from the set of sessions to discard from `attrs`.
void RemoveSessionsFromSessionsToDiscard(const SessionIds& session_ids,
                                         ProfileAttributesIOS& attrs) {
  SessionIds discarded_sessions;
  std::ranges::set_difference(
      attrs.GetDiscardedSessions(), session_ids,
      std::inserter(discarded_sessions, discarded_sessions.end()));
  for (const std::string& session_id : session_ids) {
    attrs.ClearSessionScopedPrefs(session_id);
  }
  attrs.SetDiscardedSessions(discarded_sessions);
}

}  // namespace

@interface ProfileController () <ProfileStateObserver, SceneStateObserver>
@end

@implementation ProfileController {
  // The ExtensionSearchEngineDataUpdater that ensure the changes to the
  // default search engine are propagated to the extensions.
  std::unique_ptr<ExtensionSearchEngineDataUpdater> _searchEngineDataUpdater;

  // Responsible for indexing chrome links (such as bookmarks, ...) in system
  // Spotlight index for the given profile.
  SpotlightManager* _spotlightManager;

  // ProfileManager used to load the profile and its attributes.
  raw_ptr<ProfileManagerIOS> _profileManager;

  // Flag recording whether the cookies are currently being saved or not.
  BOOL _savingCookies;

  // Keep the loaded profile alive.
  ScopedProfileKeepAliveIOS _scopedProfileKeepAlive;

  // Used to control whether the animations should be cancelled.
  base::OneShotTimer _cancelAnimationTimer;
}

- (instancetype)initWithAppState:(AppState*)appState
                 metricsMediator:(MetricsMediator*)metricsMediator {
  if ((self = [super init])) {
    _state = [[ProfileState alloc] initWithAppState:appState];
    _metricsMediator = metricsMediator;
    [_state addObserver:self];

    // Inform the AppState of the ProfileState creation.
    [appState profileStateCreated:_state];
  }
  return self;
}

- (void)loadProfileNamed:(std::string_view)profileName
            usingManager:(ProfileManagerIOS*)manager {
  CHECK_EQ(_state.initStage, ProfileInitStage::kStart);

  // Store the pointer to the profile manager.
  _profileManager = manager;

  // Transition to the next init stage before loading the profile as the
  // load may be synchronous (if the profile has already been loaded for
  // background operation).
  [_state queueTransitionToNextInitStage];

  __weak ProfileController* weakSelf = self;
  _profileManager->CreateProfileAsync(
      profileName, base::BindOnce(^(ScopedProfileKeepAliveIOS keep_alive) {
        [weakSelf profileLoaded:std::move(keep_alive)];
      }));
}

- (void)shutdown {
  // Stop and destroy the profile specific service helpers (SpotlightManager,
  // ExtensionSearchEngineDataUpdater, ...) if they have been created.
  _searchEngineDataUpdater.reset();
  [_spotlightManager shutdown];
  _spotlightManager = nil;

  // Under the UIScene API, -sceneDidDisconnect: notification is not sent to
  // the UISceneDelegate on app termination. Mark all connected scene states
  // as disconnected in order to allow the services to properly unregister
  // their observers and tear down the remaining UI.
  for (SceneState* sceneState in _state.connectedScenes) {
    sceneState.activationLevel = SceneActivationLevelDisconnected;
  }

  // Cancel any pending deferred startup tasks (the profile is shutting
  // down, so there is no point in running them).
  [_state.deferredRunner cancelAllBlocks];

  // Inform the AppState of the ProfileState destruction.
  [_state.appState profileStateDestroyed:_state];

  // Clear the -profile property of ProfileState before unloading the object.
  [_state setProfile:nullptr];

  // Destroy the ScopedProfileKeepAlive which will allow the ProfileManagerIOS
  // to unload the profile (if this was the last object keeping it alive).
  _scopedProfileKeepAlive.Reset();
}

#pragma mark ProfileStateObserver

- (void)profileState:(ProfileState*)profileState
    willTransitionToInitStage:(ProfileInitStage)nextInitStage
                fromInitStage:(ProfileInitStage)fromInitStage {
  switch (nextInitStage) {
    case ProfileInitStage::kStart:
      NOTREACHED();

    case ProfileInitStage::kLoadProfile:
    case ProfileInitStage::kPurgeDiscardedSessionsData:
    case ProfileInitStage::kProfileLoaded:
    case ProfileInitStage::kPrepareUI:
      // Nothing to do.
      break;

    case ProfileInitStage::kUIReady:
      [self startUpBeforeFirstWindowCreated];
      break;

    case ProfileInitStage::kFirstRun:
    case ProfileInitStage::kChoiceScreen:
      // Nothing to do.
      break;

    case ProfileInitStage::kNormalUI:
      [self restartAnimations];
      break;

    case ProfileInitStage::kFinal:
      // Nothing to do.
      break;
  }

  _cancelAnimationTimer.Stop();
}

- (void)profileState:(ProfileState*)profileState
    didTransitionToInitStage:(ProfileInitStage)nextInitStage
               fromInitStage:(ProfileInitStage)fromInitStage {
  switch (nextInitStage) {
    case ProfileInitStage::kStart:
      NOTREACHED();

    case ProfileInitStage::kLoadProfile:
      // Nothing to do.
      break;

    case ProfileInitStage::kPurgeDiscardedSessionsData:
      [self purgeDiscardedSessionsData];
      break;

    case ProfileInitStage::kProfileLoaded:
      [self startUpBrowserBackgroundInitialization];
      [profileState queueTransitionToNextInitStage];
      break;

    case ProfileInitStage::kPrepareUI:
      [self maybeContinueForegroundInitialization];
      break;

    case ProfileInitStage::kUIReady:
      // SceneController uses this stage to create the normal UI if needed.
      // There is no specific agent (other than SceneController) handling
      // this stage.
      [profileState queueTransitionToNextInitStage];
      break;

    case ProfileInitStage::kFirstRun:
    case ProfileInitStage::kChoiceScreen:
      [self handleBlockingInInitStage:nextInitStage];
      break;

    case ProfileInitStage::kNormalUI:
      [profileState queueTransitionToNextInitStage];
      break;

    case ProfileInitStage::kFinal:
      break;
  }
}

- (void)profileState:(ProfileState*)profileState
    firstSceneHasInitializedUI:(SceneState*)sceneState {
  DCHECK_GE(profileState.initStage, ProfileInitStage::kUIReady);
  [self startUpAfterFirstWindowCreated];
}

- (void)profileState:(ProfileState*)profileState
      sceneConnected:(SceneState*)sceneState {
  const ProfileInitStage initStage = _state.initStage;
  if (initStage >= ProfileInitStage::kUIReady) {
    return;
  }

  [sceneState addObserver:self];
}

#pragma mark SceneStateObserver

- (void)sceneState:(SceneState*)sceneState
    transitionedToActivationLevel:(SceneActivationLevel)level {
  switch (level) {
    case SceneActivationLevelUnattached:
      break;

    case SceneActivationLevelDisconnected:
      [sceneState removeObserver:self];
      break;

    case SceneActivationLevelBackground:
      break;

    case SceneActivationLevelForegroundInactive:
    case SceneActivationLevelForegroundActive:
      [self maybeContinueForegroundInitialization];
      break;
  }
}

#pragma mark AppLifetimeObserver

- (void)applicationWillResignActive:(UIApplication*)application {
  // Nothing to do if the profile is not yet fully loaded.
  if (_state.initStage < ProfileInitStage::kPrepareUI) {
    return;
  }
}

- (void)applicationWillTerminate:(UIApplication*)application {
  // Nothing to do if the profile is not yet fully loaded.
  if (_state.initStage < ProfileInitStage::kPrepareUI) {
    return;
  }

  DCHECK(_state.profile);

  // Halt the tabs, so any outstanding requests get cleaned up, without actually
  // closing the tabs. Set the BVC to inactive to cancel all the dialogs.
  for (Browser* browser :
       BrowserListFactory::GetForProfile(_state.profile)
           ->BrowsersOfType(BrowserList::BrowserType::kAll)) {
    if (auto* agent = WebUsageEnablerBrowserAgent::FromBrowser(browser)) {
      agent->SetWebUsageEnabled(false);
    }
  }
}

- (void)applicationDidEnterBackground:(UIApplication*)application
                         memoryHelper:(MemoryWarningHelper*)memoryHelper {
  // Nothing to do if the profile is not yet fully loaded.
  if (_state.initStage < ProfileInitStage::kPrepareUI) {
    return;
  }

  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;

  enterprise_idle::IdleServiceFactory::GetForProfile(profile)
      ->OnApplicationWillEnterBackground();

  // Save the cookies unless there is already a save in progress. This avoid
  // posting multiple tasks if the user switch rapidly between multiple apps.
  if (!base::FeatureList::IsEnabled(
          kDisableCookieStoreIOSFlushOnBackgrounding) &&
      !_savingCookies) {
    _savingCookies = YES;

    // Save the cookie while ensuring the application will be given time for
    // the operation to complete by marking it as a critical closure. Since
    // the closure passed to FlushCookieStoreOnIOThread(...) may be invoked
    // on an arbitrary sequence, wrap it in base::BindPostTask(...) so that
    // it executes on the current sequence.
    __weak ProfileController* weakSelf = self;
    web::GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &FlushCookieStoreOnIOThread,
            base::WrapRefCounted(profile->GetRequestContext()),
            base::BindPostTask(
                base::SequencedTaskRunner::GetCurrentDefault(),
                base::MakeCriticalClosure("-[ProfileController saveCookies]",
                                          base::BindOnce(^{
                                            [weakSelf cookiesSaved];
                                          }),
                                          /*is_immediate=*/true))));
  }
}

- (void)applicationWillEnterForeground:(UIApplication*)application
                          memoryHelper:(MemoryWarningHelper*)memoryHelper {
  // Nothing to do if the profile is not yet fully loaded.
  if (_state.initStage < ProfileInitStage::kPrepareUI) {
    return;
  }

  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;

  AuthenticationServiceFactory::GetForProfile(profile)
      ->OnApplicationWillEnterForeground();

  enterprise_idle::IdleServiceFactory::GetForProfile(profile)
      ->OnApplicationWillEnterForeground();

  if (IsMobilePromoOnDesktopRecordActiveDaysEnabled() ||
      MobilePromoOnDesktopEnabled()) {
    CrossPlatformPromosServiceFactory::GetForProfile(profile)
        ->OnApplicationWillEnterForeground();
  }

  // Send the "Chrome opened" event to the feature engagement tracker on a
  // warm start.
  [self sendChromeOpenedEvent];
}

- (void)application:(UIApplication*)application
    didDiscardSceneSessions:(NSSet<UISceneSession*>*)sceneSessions {
  NOTREACHED();
}

#pragma mark Private methods

- (void)profileLoaded:(ScopedProfileKeepAliveIOS)keepAlive {
  CHECK(!_scopedProfileKeepAlive.profile());
  _scopedProfileKeepAlive = std::move(keepAlive);
  ProfileIOS* profile = _scopedProfileKeepAlive.profile();
  CHECK(profile);

  [_state setProfile:profile];
  [_state queueTransitionToNextInitStage];
}

- (void)purgeDiscardedSessionsData {
  DCHECK(_state.profile);
  DCHECK(_profileManager);
  ProfileIOS* profile = _state.profile;

  SessionIds sessionIDs =
      _profileManager->GetProfileAttributesStorage()
          ->GetAttributesForProfileWithName(profile->GetProfileName())
          .GetDiscardedSessions();

  if (sessionIDs.empty() || tests_hook::NeverPurgeDiscardedSessionsData()) {
    // No data to purge since there is no discarded sessions, advance stage.
    [self dataPurgedForDiscardedSessions:sessionIDs];
    return;
  }

  std::array<base::FilePath, 2> storagePaths = {
      profile->GetStatePath(),
      profile->GetOffTheRecordStatePath(),
  };

  __weak ProfileController* weakSelf = self;
  base::ThreadPool::PostTaskAndReply(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&PurgeDataForSessions, sessionIDs, storagePaths),
      base::BindOnce(^{
        [weakSelf dataPurgedForDiscardedSessions:sessionIDs];
      }));
}

- (void)dataPurgedForDiscardedSessions:(const SessionIds&)sessions {
  DCHECK(_state.profile);
  DCHECK(_profileManager);
  if (!sessions.empty()) {
    _profileManager->GetProfileAttributesStorage()
        ->UpdateAttributesForProfileWithName(
            _state.profile->GetProfileName(),
            base::BindOnce(&RemoveSessionsFromSessionsToDiscard, sessions));
  }

  // The profile manager is no longer used, clear the pointer so that it
  // does not dangle.
  _profileManager = nullptr;

  [_state queueTransitionToNextInitStage];
}

- (void)startUpBrowserBackgroundInitialization {
  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;
  PrefService* prefs = profile->GetPrefs();

  // Record initial translation metrics.
  language::LanguageUsageMetrics::RecordAcceptLanguages(
      prefs->GetString(language::prefs::kAcceptLanguages));
  translate::TranslateMetricsLoggerImpl::LogApplicationStartMetrics(
      ChromeIOSTranslateClient::CreateTranslatePrefs(prefs));


  // Force desktop mode when racoon is enabled.
  if (ios::provider::IsRaccoonEnabled()) {
    if (!prefs->GetBoolean(prefs::kUserAgentWasChanged)) {
      prefs->SetBoolean(prefs::kUserAgentWasChanged, true);
      ios::HostContentSettingsMapFactory::GetForProfile(profile)
          ->SetDefaultContentSetting(ContentSettingsType::REQUEST_DESKTOP_SITE,
                                     CONTENT_SETTING_ALLOW);
    }
  }

  if (!tests_hook::ShouldLoadMinimalAppUI()) {
    [self attachProfileAgents];
  }
}

- (void)attachProfileAgents {
  [_state addAgent:[[OTRPRofileDestroyerProfileAgent alloc] init]];
  [_state addAgent:[[CertificatePolicyProfileAgent alloc] init]];
  [_state addAgent:[[FirstRunProfileAgent alloc] init]];
  [_state addAgent:[[MultiProfileForcedMigrationProfileAgent alloc] init]];
  [_state addAgent:[[IdentityConfirmationProfileAgent alloc] init]];
  [_state addAgent:[[ProfileActivityProfileAgent alloc] init]];
  [_state addAgent:[[PostRestoreProfileAgent alloc] init]];
  [_state addAgent:[[SearchEngineChoiceProfileAgent alloc] init]];
  [_state addAgent:[[SessionMetricsProfileAgent alloc] init]];

  if (IsDockingPromoV2Enabled()) {
    [_state addAgent:[[AppIconLaunchedProfileAgent alloc] init]];
  }

  if (IsWelcomeBackEnabled()) {
    [_state addAgent:[[WelcomeBackScreenProfileAgent alloc] init]];
  }

  [_state addAgent:[[SyncedSetUpProfileAgent alloc] init]];

  if (IsIOSBackendPromoServiceIntegrationEnabled()) {
    [_state addAgent:[[BackendPromoProfileAgent alloc] init]];
  }

  [_state addAgent:[[HomeBackgroundCustomizationPromoProfileAgent alloc] init]];
}

- (void)maybeContinueForegroundInitialization {
  if (_state.initStage != ProfileInitStage::kPrepareUI) {
    return;
  }

  if (_state.foregroundScenes.count == 0) {
    return;
  }

  // Stop listening to the SceneStates, as there is no need anymore once
  // the transition to the next stage is scheduled. This avoids a crash
  // if a SceneState reaches foreground in reaction to the ProfileState
  // reaching the PrepareUI stage.
  for (SceneState* sceneState in _state.connectedScenes) {
    [sceneState removeObserver:self];
  }

  [_state queueTransitionToNextInitStage];
}

- (void)startUpBeforeFirstWindowCreated {
  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;

  // Send "Chrome Opened" event to the feature engagement tracker on
  // cold start.
  [self sendChromeOpenedEvent];

  _spotlightManager = [SpotlightManager spotlightManagerWithProfile:profile];

#if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)
  CredentialProviderServiceFactory::GetForProfile(profile);
#endif
}

// Schedule a task to execute in one run loop that will cancel all in-progress
// animation on all connected scenes if the init stage has not progressed. It
// is part of the contract of those stages that the transition must either be
// instantaneous or require user interaction (and thus the animations have to
// be cancelled).
- (void)handleBlockingInInitStage:(ProfileInitStage)initStage {
  CHECK_GT(initStage, ProfileInitStage::kUIReady);
  CHECK_LT(initStage, ProfileInitStage::kNormalUI);

  __weak ProfileController* weakSelf = self;
  _cancelAnimationTimer.Start(FROM_HERE, base::Seconds(0), base::BindOnce(^{
                                [weakSelf cancelAnimationsIfInStage:initStage];
                              }));
}

// Cancel animations on all connected scenes if the current init state is
// equal to `initStage`. Scheduled by -handleBlockingInInitStage: to execute
// after a delay of one run loop.
- (void)cancelAnimationsIfInStage:(ProfileInitStage)initStage {
  CHECK_GT(initStage, ProfileInitStage::kUIReady);
  CHECK_LT(initStage, ProfileInitStage::kNormalUI);

  if (_state.initStage == initStage) {
    for (SceneState* sceneState in _state.connectedScenes) {
      [sceneState.animator cancelAnimation];
    }
  }
}

// Restart animations for all connected scenes (if necessary).
- (void)restartAnimations {
  for (SceneState* sceneState in _state.connectedScenes) {
    [sceneState.animator restartAnimation];
  }
}

- (void)startUpAfterFirstWindowCreated {
  [self scheduleRemoveExternalFiles];
  [self scheduleCreateSearchEngineDataUpdater];
  [self scheduleClearingSessionCookies];
  [self scheduleCleanupSessionStateCache];
  [self scheduleCreateMailtoHandlerService];
  [self scheduleCleanupReadingListOfflineData];
  [self scheduleResyncSpotlightIndex];
  [self scheduleCleanupFavicons];
  [self scheduleLogStorageMetrics];

  // The UI is created when a window is in the foreground and the Profile
  // initialisation has progressed enough. Since the window coming to the
  // foreground could have happened a while ago, notify of the foreground
  // event at this point.
  [self applicationWillEnterForeground];
}

- (void)applicationWillEnterForeground {
  DCHECK(_state.profile);
  enterprise_idle::IdleServiceFactory::GetForProfile(_state.profile)
      ->OnApplicationWillEnterForeground();
  if (IsMobilePromoOnDesktopRecordActiveDaysEnabled() ||
      MobilePromoOnDesktopEnabled()) {
    CrossPlatformPromosServiceFactory::GetForProfile(_state.profile)
        ->OnApplicationWillEnterForeground();
  }
}

- (void)sendChromeOpenedEvent {
  DCHECK(_state.profile);
  feature_engagement::Tracker* tracker =
      feature_engagement::TrackerFactory::GetForProfile(_state.profile);

  tracker->NotifyEvent(feature_engagement::events::kChromeOpened);
  [_metricsMediator notifyCredentialProviderWasUsed:tracker];
}

- (void)cookiesSaved {
  _savingCookies = NO;
}

#pragma mark Deferred initialization tasks scheduling

// Schedules external files removal.
- (void)scheduleRemoveExternalFiles {
  DCHECK(_state.profile);
  ExternalFileRemoverFactory::GetForProfile(_state.profile)
      ->RemoveAfterDelay(kExternalFilesCleanupDelay, base::DoNothing());
}

// Schedules installation of an ExtensionSearchEngineDataUpdater to track
// the changes to the default search engine.
- (void)scheduleCreateSearchEngineDataUpdater {
  DCHECK(_state.deferredRunner);
  __weak ProfileController* weakSelf = self;
  [_state.deferredRunner enqueueBlockNamed:kStartupInitPrefObservers
                                     block:^{
                                       [weakSelf createSearchEngineDataUpdater];
                                     }];
}

// Schedules clearing the session cookies.
- (void)scheduleClearingSessionCookies {
  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;
  if (cookie_util::ShouldClearSessionCookies(profile->GetPrefs())) {
    cookie_util::ClearSessionCookies(profile);
    if (profile->HasOffTheRecordProfile()) {
      cookie_util::ClearSessionCookies(profile->GetOffTheRecordProfile());
    }
  }
}

// Schedules deleting leftover session state cache files.
- (void)scheduleCleanupSessionStateCache {
  DCHECK(_state.deferredRunner);
  __weak ProfileController* weakSelf = self;
  [_state.deferredRunner enqueueBlockNamed:kStartupPurgeUnassociatedData
                                     block:^{
                                       [weakSelf cleanupSessionStateCache];
                                     }];
}

// Schedules creating the MailtoHandlerService instance.
- (void)scheduleCreateMailtoHandlerService {
  DCHECK(_state.deferredRunner);
  __weak ProfileController* weakSelf = self;
  [_state.deferredRunner enqueueBlockNamed:kStartupCreateMailtoHandlerService
                                     block:^{
                                       [weakSelf createMailtoHandlerService];
                                     }];
}

// Schedules cleanup of the ReadingList offline data directory.
// TODO(crbug.com/522229299): Remove after Jun 2027.
- (void)scheduleCleanupReadingListOfflineData {
  DCHECK(_state.deferredRunner);
  __weak ProfileController* weakSelf = self;
  [_state.deferredRunner enqueueBlockNamed:kStartupCleanupReadingListOfflineData
                                     block:^{
                                       [weakSelf cleanupReadingListOfflineData];
                                     }];
}

// Schedules resynchronisation of the Spotlight index.
- (void)scheduleResyncSpotlightIndex {
  DCHECK(_state.deferredRunner);
  __weak ProfileController* weakSelf = self;
  [_state.deferredRunner enqueueBlockNamed:kStartResyncSpotlightIndex
                                     block:^{
                                       [weakSelf resyncSpotlightIndex];
                                     }];
}

// Schedules cleaning up favicons.
- (void)scheduleCleanupFavicons {
#if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)
  DCHECK(_state.deferredRunner);
  __weak ProfileController* weakSelf = self;
  [_state.deferredRunner enqueueBlockNamed:kStartupCleanupFavicons
                                     block:^{
                                       [weakSelf cleanupFavicons];
                                     }];
#endif
}

// Schedules logging the storage metrics.
- (void)scheduleLogStorageMetrics {
#if !TARGET_OS_SIMULATOR
  if (!base::FeatureList::IsEnabled(kLogApplicationStorageSizeMetrics)) {
    return;
  }

  DCHECK(_state.deferredRunner);
  __weak ProfileController* weakSelf = self;
  [_state.deferredRunner enqueueBlockNamed:kStartupLogStorageMetrics
                                     block:^{
                                       [weakSelf logStorageMetrics];
                                     }];
#endif
}

#pragma mark Deferred initialisation tasks

// Ensures changes to the default search engine are propagated to extensions.
- (void)createSearchEngineDataUpdater {
  DCHECK(_state.profile);
  _searchEngineDataUpdater = std::make_unique<ExtensionSearchEngineDataUpdater>(
      ios::TemplateURLServiceFactory::GetForProfile(_state.profile));
}

// Ensures obsolete session state cache files are deleted.
- (void)cleanupSessionStateCache {
  DCHECK(_state.profile);
  SessionRestorationServiceFactory::GetForProfile(_state.profile)
      ->PurgeUnassociatedData(base::DoNothing());
}

// Ensures the MailtoHandlerService is instantiated.
- (void)createMailtoHandlerService {
  DCHECK(_state.profile);
  std::ignore = MailtoHandlerServiceFactory::GetForProfile(_state.profile);
}

// Cleans up the ReadingList offline data directory.
- (void)cleanupReadingListOfflineData {
  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;
  base::FilePath offline_directory =
      profile->GetStatePath().Append(FILE_PATH_LITERAL("Offline"));
  base::ThreadPool::PostTask(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(base::IgnoreResult(&base::DeletePathRecursively),
                     offline_directory));
}

// Resynchronizes the spotlight index.
- (void)resyncSpotlightIndex {
  [_spotlightManager resyncIndex];
}

#if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)
// Ensures favicons are cleaned up.
- (void)cleanupFavicons {
  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;

  // Only use the fallback to the Google server when fetching favicons for
  // normal encryption users saving to the account, because they are the only
  // users who consented to share data to Google.
  const bool fallbackToGoogleServer =
      password_manager_util::IsSavingPasswordsToAccountWithNormalEncryption(
          SyncServiceFactory::GetForProfile(profile));

  if (fallbackToGoogleServer) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(&UpdateFaviconsStorageForProfile, profile->AsWeakPtr(),
                       fallbackToGoogleServer));
  }
}
#endif  // BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)

#if !TARGET_OS_SIMULATOR
// Logs storage metrics.
- (void)logStorageMetrics {
  DCHECK(_state.profile);
  ProfileIOS* profile = _state.profile;
  PrefService* prefService = profile->GetPrefs();
  if (!ShouldLogStorageMetrics(prefService)) {
    return;
  }

  prefService->SetTime(prefs::kLastApplicationStorageMetricsLogTime,
                       base::Time::Now());
  LogApplicationStorageMetrics(profile->GetStatePath(),
                               profile->GetOffTheRecordStatePath());
}
#endif  // !TARGET_OS_SIMULATOR

@end
