// Copyright 2022 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/browser/push_notification/model/push_notification_delegate.h"

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/check_is_test.h"
#import "base/files/file_path.h"
#import "base/functional/callback_helpers.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "base/timer/timer.h"
#import "base/values.h"
#import "components/prefs/pref_service.h"
#import "components/sync_device_info/device_info_sync_service.h"
#import "google_apis/gaia/gaia_id.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/app_state_observer.h"
#import "ios/chrome/app/change_profile_commands.h"
#import "ios/chrome/app/change_profile_continuation.h"
#import "ios/chrome/app/profile/profile_init_stage.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/app/profile/profile_state_observer.h"
#import "ios/chrome/app/startup/app_launch_metrics.h"
#import "ios/chrome/browser/content_notification/model/content_notification_nau_configuration.h"
#import "ios/chrome/browser/content_notification/model/content_notification_service.h"
#import "ios/chrome/browser/content_notification/model/content_notification_service_factory.h"
#import "ios/chrome/browser/content_notification/model/content_notification_settings_action.h"
#import "ios/chrome/browser/content_notification/model/content_notification_util.h"
#import "ios/chrome/browser/push_notification/model/constants.h"
#import "ios/chrome/browser/push_notification/model/notification_metrics_recorder.h"
#import "ios/chrome/browser/push_notification/model/provisional_push_notification_service.h"
#import "ios/chrome/browser/push_notification/model/provisional_push_notification_service_factory.h"
#import "ios/chrome/browser/push_notification/model/push_notification_account_context_manager.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client_id.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client_manager.h"
#import "ios/chrome/browser/push_notification/model/push_notification_configuration.h"
#import "ios/chrome/browser/push_notification/model/push_notification_profile_service.h"
#import "ios/chrome/browser/push_notification/model/push_notification_profile_service_factory.h"
#import "ios/chrome/browser/push_notification/model/push_notification_service.h"
#import "ios/chrome/browser/push_notification/model/push_notification_settings_util.h"
#import "ios/chrome/browser/push_notification/model/push_notification_util.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state_observer.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.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/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.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/model/utils/first_run_util.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/scene_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/account_profile_mapper.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/sync/model/device_info_sync_service_factory.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "third_party/search_engines_data/resources/definitions/prepopulated_engines.h"

namespace {
// The time range's expected min and max values for custom histograms.
constexpr base::TimeDelta kTimeRangeIncomingNotificationHistogramMin =
    base::Milliseconds(1);
constexpr base::TimeDelta kTimeRangeIncomingNotificationHistogramMax =
    base::Seconds(30);
// Number of buckets for the time range histograms.
constexpr int kTimeRangeHistogramBucketCount = 30;

// The histogram used to record a push notification's current lifecycle state on
// the device.
const char kLifecycleEventsHistogram[] = "IOS.PushNotification.LifecyleEvents";

// This enum is used to represent a point along the push notification's
// lifecycle.
enum class PushNotificationLifecycleEvent {
  kNotificationReception,
  kNotificationForegroundPresentation,
  kNotificationInteraction,
  kMaxValue = kNotificationInteraction
};

BASE_FEATURE_PARAM(int,
                   kDeliveredNAUMaxPerSessionFeature,
                   &kContentNotificationDeliveredNAU,
                   kDeliveredNAUMaxPerSession,
                   kDeliveredNAUMaxSendsPerSession);

// Extract the notification information from `attr`, and store them into
// `mapping`. Will also copy the notification permission from the profile's
// pref into the `attr` if the profile is loaded.
void ExtractNotificationInformation(ProfileManagerIOS* manager,
                                    NSMutableDictionary* mapping,
                                    ProfileAttributesIOS& attr) {
  const GaiaId& gaia_id = attr.GetGaiaId();
  if (gaia_id.empty()) {
    return;
  }

  // Get the permissions from `attr` but if they are missing, check if they
  // can be found in the profile (if it is loaded).
  const base::DictValue* permissions = attr.GetNotificationPermissions();
  if (!permissions) {
    ProfileIOS* profile = manager->GetProfileWithName(attr.GetProfileName());
    if (profile) {
      const base::DictValue& profile_permissions = profile->GetPrefs()->GetDict(
          prefs::kFeaturePushNotificationPermissions);
      attr.SetNotificationPermissions(profile_permissions.Clone());
      permissions = attr.GetNotificationPermissions();
    }
  }

  // There is no permissions for the profile in attr (or no permission could
  // be copied from the profile's pref, possibly because the profile is not
  // yet loaded).
  if (!permissions) {
    return;
  }

  NSMutableDictionary* permissions_map = [[NSMutableDictionary alloc] init];
  for (const auto pair : *permissions) {
    permissions_map[base::SysUTF8ToNSString(pair.first)] =
        [NSNumber numberWithBool:pair.second.GetBool()];
  }

  mapping[gaia_id.ToNSString()] = permissions_map;
}

// This function creates a dictionary that maps signed-in user's GAIA IDs to a
// map of each user's preferences for each push notification enabled feature.
GaiaIdToPushNotificationPreferenceMap*
GaiaIdToPushNotificationPreferenceMapFromCache() {
  ProfileManagerIOS* manager = GetApplicationContext()->GetProfileManager();
  ProfileAttributesStorageIOS* storage = manager->GetProfileAttributesStorage();

  NSMutableDictionary* account_preference_map =
      [[NSMutableDictionary alloc] init];

  storage->IterateOverProfileAttributes(base::BindRepeating(
      &ExtractNotificationInformation, manager, account_preference_map));

  return account_preference_map;
}

// Call ContentNotificationService::SendNAUForConfiguration() after fetching
// the notification settings if `weak_profile` is still valid.
void SendNAUFConfigurationForProfileWithSettings(
    base::WeakPtr<ProfileIOS> weak_profile,
    UNNotificationSettings* settings) {
  ProfileIOS* profile = weak_profile.get();
  if (!profile) {
    return;
  }

  UNAuthorizationStatus previousAuthStatus =
      [PushNotificationUtil getSavedPermissionSettings];
  ContentNotificationNAUConfiguration* config =
      [[ContentNotificationNAUConfiguration alloc] init];
  ContentNotificationSettingsAction* settingsAction =
      [[ContentNotificationSettingsAction alloc] init];
  settingsAction.previousAuthorizationStatus = previousAuthStatus;
  settingsAction.currentAuthorizationStatus = settings.authorizationStatus;
  config.settingsAction = settingsAction;
  ContentNotificationServiceFactory::GetForProfile(profile)
      ->SendNAUForConfiguration(config);
}

// Records a failure to access the PushNotificationClientManager at a specific
// point.
void RecordClientManagerAccessFailure(
    PushNotificationClientManagerFailurePoint failure_point) {
  base::UmaHistogramEnumeration(
      "IOS.PushNotification.ClientManagerAccessFailure", failure_point);
}

// Records the outcome of handling a multi-profile notification interaction.
void RecordPushNotificationTargetProfileHandlingResult(
    PushNotificationTargetProfileHandlingResult result) {
  base::UmaHistogramEnumeration("IOS.PushNotification.MultiProfile."
                                "PushNotificationTargetProfileHandlingResult",
                                result);
}

// Helper function to get the profile-specific PushNotificationClientManager
// directly from a ProfileIOS object. Returns nullptr if the manager cannot be
// retrieved.
PushNotificationClientManager* GetClientManagerForProfile(ProfileIOS* profile) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  if (!profile) {
    RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
                                         kGetClientManagerNullProfileInput);

    return nullptr;
  }

  PushNotificationProfileService* profile_service =
      PushNotificationProfileServiceFactory::GetForProfile(profile);

  if (!profile_service) {
    RecordClientManagerAccessFailure(
        PushNotificationClientManagerFailurePoint::
            kGetClientManagerMissingProfileService);

    return nullptr;
  }

  return profile_service->GetPushNotificationClientManager();
}

// Determines the associated Profile name using `user_info`.
//
// It first looks for `kOriginatingProfileNameKey`. If absent or otherwise
// invalid, it falls back to checking `kOriginatingGaiaIDKey` and uses
// `AccountProfileMapper` to map the Gaia ID to a Profile name.
//
// Returns the Profile name if found, otherwise returns an empty string. Logs
// specific reasons for failure to UMA.
//
// Note: This function should only be called when
// `IsMultiProfilePushNotificationHandlingEnabled()` is true.
std::string GetProfileNameFromUserInfo(NSDictionary* user_info) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  ProfileManagerIOS* profile_manager =
      GetApplicationContext()->GetProfileManager();

  if (!profile_manager) {
    CHECK_IS_TEST();
    return "";
  }

  NSString* profile_name_ns = user_info[kOriginatingProfileNameKey];

  if (profile_name_ns) {
    if (profile_name_ns.length == 0) {
      RecordClientManagerAccessFailure(
          PushNotificationClientManagerFailurePoint::
              kGetProfileNameEmptyNameProvided);
      // Definite failure: An empty Profile name was explicitly provided. Cannot
      // proceed or fallback.
      return "";
    }

    std::string profile_name = base::SysNSStringToUTF8(profile_name_ns);

    if (!profile_manager->HasProfileWithName(profile_name)) {
      RecordClientManagerAccessFailure(
          PushNotificationClientManagerFailurePoint::
              kGetProfileNameDirectNameNotFoundInStorage);
      // Definite failure: An invalid Profile name was explicitly provided.
      // Cannot proceed or fallback.
      return "";
    }

    // Definite success: Found a valid, existing Profile name directly via
    // `kOriginatingProfileNameKey`.
    return profile_name;
  }

  NSString* gaia_id_ns = user_info[kOriginatingGaiaIDKey];
  // TODO(crbug.com/524713899): Rely on this Chime payload key for all Chime
  // notifications, not just Content Push Notifications.
  if (!gaia_id_ns.length && IsContentPushNotificationsEnabled()) {
    NSDictionary* chime_payload = user_info[kSerializedChimePayloadKey];
    if (chime_payload) {
      id user_id_obj = chime_payload[kChimeNotificationGaiaIDKey];
      if (user_id_obj) {
        gaia_id_ns = [NSString stringWithFormat:@"%@", user_id_obj];
      }
    }
  }
  GaiaId gaia_id = GaiaId(gaia_id_ns);

  if (gaia_id.empty()) {
    RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
                                         kGetProfileNameMissingOrEmptyGaiaID);
    // Definite failure: The string provided for kOriginatingGaiaIDKey was
    // either missing or empty.
    return "";
  }

  std::optional<std::string> mapped_profile_name =
      GetApplicationContext()
          ->GetAccountProfileMapper()
          ->FindProfileNameForGaiaID(gaia_id);

  if (!mapped_profile_name.has_value()) {
    RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
                                         kGetProfileNameGaiaIdNotMapped);
    // Definite failure: The Gaia ID was valid but is not associated with any
    // known Profile according to the AccountProfileMapper.
    return "";
  }

  if (!profile_manager->HasProfileWithName(mapped_profile_name.value())) {
    RecordClientManagerAccessFailure(
        PushNotificationClientManagerFailurePoint::
            kGetProfileNameMappedNameNotFoundInStorage);
    // Definite failure: Gaia ID mapped successfully to a Profile name, but that
    // Profile name does not exist in ProfileAttributesStorageIOS (e.g., stale
    // mapping or recently deleted profile).
    return "";
  }

  return mapped_profile_name.value();
}

// Callback used to asynchronously retrieve a `PushNotificationClientManager`.
using ClientManagerCallback =
    base::OnceCallback<void(PushNotificationClientManager*)>;

// Helper function invoked after an asynchronous Profile load attempt.
// It retrieves the `PushNotificationClientManager` for the loaded Profile (if
// successful) and runs the original callback with the result.
//
// TODO(crbug.com/418696432): Ensure that the ScopedProfileKeepAliveIOS is
// not destroyed before `original_callback` and any background processing,
// if any, is complete. Currently the profile will be unloaded as soon as
// the current function returns, even if `original_callback` starts any
// background processing.
void OnProfileLoadedForClientManager(ClientManagerCallback original_callback,
                                     ScopedProfileKeepAliveIOS keep_alive) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  ProfileIOS* profile_after_load = keep_alive.profile();
  if (!profile_after_load) {
    RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
                                         kGetClientManagerProfileLoadFailed);
    std::move(original_callback).Run(nullptr);
    return;
  }

  PushNotificationClientManager* manager =
      GetClientManagerForProfile(profile_after_load);

  std::move(original_callback).Run(manager);
}

// Gets the appropriate `PushNotificationClientManager` based on `user_info`.
// Checks for Profile-identifying keys (`kOriginatingProfileNameKey`,
// `kOriginatingGaiaIDKey`). If neither key is present, it synchronously returns
// the app-wide manager via the `callback`. If keys are present, it attempts to
// retrieve the Profile-specific manager, potentially loading the Profile
// asynchronously. The callback receives the retrieved manager or `nullptr` if
// the Profile-specific lookup or load fails.
void GetClientManagerForUserInfo(NSDictionary* user_info,
                                 ClientManagerCallback callback) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  BOOL hasProfileKey = (user_info[kOriginatingProfileNameKey] != nil);
  BOOL hasGaiaKey = (user_info[kOriginatingGaiaIDKey] != nil);

  // If the notification payload contains neither the originating Profile name
  // key (`kOriginatingProfileNameKey`) nor the originating Gaia ID key
  // (`kOriginatingGaiaIDKey`), assume it's intended only for the app-wide
  // client manager.
  if (!hasProfileKey && !hasGaiaKey) {
    PushNotificationClientManager* app_wide_manager =
        GetApplicationContext()
            ->GetPushNotificationService()
            ->GetPushNotificationClientManager();

    std::move(callback).Run(app_wide_manager);

    return;
  }

  std::string profile_name = GetProfileNameFromUserInfo(user_info);

  if (profile_name.empty()) {
    RecordClientManagerAccessFailure(
        PushNotificationClientManagerFailurePoint::
            kGetClientManagerFailedToGetProfileName);
    std::move(callback).Run(nullptr);

    return;
  }

  ProfileManagerIOS* profile_manager =
      GetApplicationContext()->GetProfileManager();

  ProfileIOS* loaded_profile =
      profile_manager->GetProfileWithName(profile_name);

  if (loaded_profile) {
    PushNotificationClientManager* manager =
        GetClientManagerForProfile(loaded_profile);
    std::move(callback).Run(manager);

    return;
  }

  if (!profile_manager->HasProfileWithName(profile_name)) {
    RecordClientManagerAccessFailure(
        PushNotificationClientManagerFailurePoint::
            kGetClientManagerProfileNotFoundByName);
    std::move(callback).Run(nullptr);

    return;
  }

  profile_manager->LoadProfileAsync(
      profile_name,
      base::BindOnce(&OnProfileLoadedForClientManager, std::move(callback)));
}

// Callback executed after a Profile switch initiated by a notification
// interaction. This function validates the state of the `new_scene_state` and
// its associated Profile, retrieves the appropriate
// `PushNotificationClientManager` for the now-active `switched_profile`, and
// forwards the original `response` for handling by that manager. Logs failures
// to UMA.
void HandleNotificationInteractionAfterProfileSwitch(
    UNNotificationResponse* response,
    SceneState* new_scene_state,
    base::OnceClosure completion_closure) {
  base::ScopedClosureRunner run_completion_closure(
      std::move(completion_closure));

  CHECK(new_scene_state.profileState);
  CHECK_GE(new_scene_state.profileState.initStage,
           ProfileInitStage::kProfileLoaded);

  ProfileIOS* switched_profile = new_scene_state.profileState.profile;
  CHECK(switched_profile);

  PushNotificationClientManager* client_manager =
      GetClientManagerForProfile(switched_profile);

  if (!client_manager) {
    RecordClientManagerAccessFailure(
        PushNotificationClientManagerFailurePoint::
            kInteractionContinuationMissingClientManager);

    return;
  }

  client_manager->HandleNotificationInteraction(response);

  // Also allow the app-scoped clients the opportunity to handle interactions.
  GetApplicationContext()
      ->GetPushNotificationService()
      ->GetPushNotificationClientManager()
      ->HandleNotificationInteraction(response);
}

// Creates a `ChangeProfileContinuation` callback bound with the original
// notification `response`. `HandleNotificationInteractionAfterProfileSwitch()`
// will be invoked by the Profile switching mechanism if a switch occurs,
// allowing the notification interaction to be handled in the context of the
// newly switched Profile.
ChangeProfileContinuation CreateNotificationInteractionContinuation(
    UNNotificationResponse* response) {
  return base::BindOnce(&HandleNotificationInteractionAfterProfileSwitch,
                        response);
}

// Handles notification reception using the app-wide client manager and calls
// the final completion block.
void HandleNotificationReceptionWithAppWideManager(
    NSDictionary* user_info,
    void (^completion_block)(UIBackgroundFetchResult /* result */)) {
  PushNotificationClientManager* app_wide_manager =
      GetApplicationContext()
          ->GetPushNotificationService()
          ->GetPushNotificationClientManager();
  CHECK(app_wide_manager);

  UIBackgroundFetchResult result =
      app_wide_manager->HandleNotificationReception(user_info);

  if (completion_block) {
    completion_block(result);
  }
}

// Callback invoked after asynchronously attempting to retrieve the
// Profile-specific `PushNotificationClientManager`. Falls back to app-wide
// manager, if necessary.
void OnClientManagerReadyForReception(
    NSDictionary* user_info,
    PushNotificationClientManagerFailurePoint failure_point,
    void (^completion_block)(UIBackgroundFetchResult result),
    PushNotificationClientManager* client_manager) {
  if (!client_manager) {
    RecordClientManagerAccessFailure(failure_point);

    if (completion_block) {
      completion_block(UIBackgroundFetchResultNoData);
    }

    return;
  }

  UIBackgroundFetchResult result =
      client_manager->HandleNotificationReception(user_info);

  if (completion_block) {
    completion_block(result);
  }
}

// Processes an incoming notification by attempting to use a Profile-specific
// client manager, falling back to the app-wide manager, if necessary.
void ProcessIncomingNotification(
    NSDictionary* user_info,
    PushNotificationClientManagerFailurePoint failure_point,
    void (^completion_block)(UIBackgroundFetchResult result)) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  ClientManagerCallback manager_ready_callback =
      base::BindOnce(&OnClientManagerReadyForReception, user_info,
                     failure_point, completion_block);

  // Start the async process to get the Profile-specific manager
  GetClientManagerForUserInfo(user_info, std::move(manager_ready_callback));
}

}  // anonymous namespace

@interface PushNotificationDelegate () <AppStateObserver,
                                        NotificationClassifier,
                                        ProfileStateObserver,
                                        SceneStateObserver>

// The first connected scene that is foreground active, or nil if there are
// none.
@property(nonatomic, readonly) SceneState* foregroundActiveScene;

// The client manager for notification clients that are app-scoped (rather than
// profile-scoped).
@property(nonatomic, readonly)
    PushNotificationClientManager* appWideClientManager;

// The object that records metrics about push notifications.
@property(nonatomic, readonly) NotificationMetricsRecorder* metricsRecorder;

@end

@implementation PushNotificationDelegate {
  __weak AppState* _appState;
  // Stores blocks to execute once the app is finished foregrounding.
  NSMutableArray<ProceduralBlock>* _runAfterForeground;
}

- (instancetype)initWithAppState:(AppState*)appState
          userNotificationCenter:
              (UNUserNotificationCenter*)userNotificationCenter {
  if ((self = [super init])) {
    _appState = appState;
    [_appState addObserver:self];
    _metricsRecorder = [[NotificationMetricsRecorder alloc]
        initWithNotificationCenter:userNotificationCenter];
    _metricsRecorder.classifier = self;
  }
  return self;
}

#pragma mark - UNUserNotificationCenterDelegate -

- (void)userNotificationCenter:(UNUserNotificationCenter*)center
    didReceiveNotificationResponse:(UNNotificationResponse*)response
             withCompletionHandler:(void (^)(void))completionHandler {
  // This method is invoked by iOS to process the user's response to a delivered
  // notification.
  [self recordLifeCycleEvent:PushNotificationLifecycleEvent::
                                 kNotificationInteraction];
  __weak __typeof(self) weakSelf = self;
  [self executeWhenForeground:^{
    [weakSelf handleNotificationResponse:response];
    [weakSelf.metricsRecorder recordInteraction:response.notification];
  }];
  // TODO(crbug.com/401537165): Consider changing when completionHandler is
  // called.
  if (completionHandler) {
    completionHandler();
  }
  base::UmaHistogramEnumeration(kAppLaunchSource,
                                AppLaunchSource::NOTIFICATION);
}

- (void)userNotificationCenter:(UNUserNotificationCenter*)center
       willPresentNotification:(UNNotification*)notification
         withCompletionHandler:
             (void (^)(UNNotificationPresentationOptions options))
                 completionHandler {
  __weak __typeof(self) weakSelf = self;
  [self executeWhenForeground:^{
    [weakSelf handleWillPresentNotification:notification
                      withCompletionHandler:completionHandler];
  }];
}

- (void)userNotificationCenter:(UNUserNotificationCenter*)center
    openSettingsForNotification:(UNNotification*)notification {
  __weak __typeof(self) weakSelf = self;
  if (IsMultiProfilePushNotificationHandlingEnabled()) {
    std::string profileName =
        GetProfileNameFromUserInfo(notification.request.content.userInfo);
    if (!profileName.empty()) {
      [self executeWhenForeground:^{
        [weakSelf openSettingsForNotification:notification
                                  profileName:profileName];
      }];
      return;
    }
  }
  [self executeWhenForeground:^{
    [weakSelf openSettingsForNotification:notification
                                    scene:weakSelf.foregroundActiveScene
                               completion:base::DoNothing()];
  }];
}

#pragma mark - PushNotificationDelegate

- (void)applicationWillProcessIncomingRemoteNotification:(NSDictionary*)userInfo
                                  fetchCompletionHandler:
                                      (void (^)(UIBackgroundFetchResult result))
                                          completionHandler {
  [self recordLifeCycleEvent:PushNotificationLifecycleEvent::
                                 kNotificationReception];

  double incomingNotificationTime =
      base::Time::Now().InSecondsFSinceUnixEpoch();

  auto recordMetricsAndComplete = ^(UIBackgroundFetchResult result) {
    double processingTime =
        base::Time::Now().InSecondsFSinceUnixEpoch() - incomingNotificationTime;

    UmaHistogramCustomTimes(
        "IOS.PushNotification.IncomingNotificationProcessingTime",
        base::Milliseconds(processingTime),
        kTimeRangeIncomingNotificationHistogramMin,
        kTimeRangeIncomingNotificationHistogramMax,
        kTimeRangeHistogramBucketCount);

    if (completionHandler) {
      completionHandler(result);
    }
  };

  if (IsMultiProfilePushNotificationHandlingEnabled()) {
    ProcessIncomingNotification(userInfo,
                                PushNotificationClientManagerFailurePoint::
                                    kWillProcessIncomingRemoteNotification,
                                recordMetricsAndComplete);
  } else {
    HandleNotificationReceptionWithAppWideManager(userInfo,
                                                  recordMetricsAndComplete);
  }
}

- (void)applicationDidRegisterWithAPNS:(NSData*)deviceToken
                               profile:(ProfileIOS*)profile {
  GaiaIdToPushNotificationPreferenceMap* accountPreferenceMap =
      GaiaIdToPushNotificationPreferenceMapFromCache();

  // Return early if no accounts are signed into Chrome.
  if (!accountPreferenceMap.count) {
    return;
  }

  if (IsMultiProfilePushNotificationHandlingEnabled()) {
    PushNotificationClientManager* clientManager =
        GetClientManagerForProfile(profile);

    // Gracefully handle the case where a clientManager couldn't be retrieved
    // (e.g., if the Profile is `nullptr` or its service isn't available).
    if (clientManager) {
      // Registers Chrome's PushNotificationClients' Actionable Notifications
      // with iOS.
      clientManager->RegisterActionableNotifications();
    } else {
      RecordClientManagerAccessFailure(
          PushNotificationClientManagerFailurePoint::kDidRegisterWithAPNS);
    }
  }

  // Registers Chrome's PushNotificationClients' Actionable Notifications with
  // iOS.
  self.appWideClientManager->RegisterActionableNotifications();

  PushNotificationConfiguration* config =
      [[PushNotificationConfiguration alloc] init];

  config.accountIDs = accountPreferenceMap.allKeys;
  config.preferenceMap = accountPreferenceMap;
  config.deviceToken = deviceToken;
  config.singleSignOnService =
      GetApplicationContext()->GetSingleSignOnService();

  if (profile) {
    config.shouldRegisterContentNotification =
        [self isContentNotificationAvailable:profile];
    if (config.shouldRegisterContentNotification) {
      AuthenticationService* authService =
          AuthenticationServiceFactory::GetForProfile(profile);
      id<SystemIdentity> identity = authService->GetPrimaryIdentity();
      config.primaryAccount = identity;
      // Send an initial NAU to share the OS auth status and channel status with
      // the server. Send an NAU on every foreground to report the OS Auth
      // Settings.
      [self sendSettingsChangeNAUForProfile:profile];
    }
  }

  __weak __typeof(self) weakSelf = self;
  base::WeakPtr<ProfileIOS> weakProfile =
      profile ? profile->AsWeakPtr() : base::WeakPtr<ProfileIOS>{};
  PushNotificationService* notificationService =
      GetApplicationContext()->GetPushNotificationService();

  notificationService->RegisterDevice(config, ^(NSError* error) {
    [weakSelf deviceRegistrationForProfile:weakProfile withError:error];
  });
}

- (void)deviceRegistrationForProfile:(base::WeakPtr<ProfileIOS>)weakProfile
                           withError:(NSError*)error {
  base::UmaHistogramBoolean("IOS.PushNotification.ChimeDeviceRegistration",
                            !error);
  if (!error) {
    if (ProfileIOS* profile = weakProfile.get()) {
        [self setUpAndEnableSendTabNotificationsWithProfile:profile];
    }
  }
}

#pragma mark - NotificationClassifier

- (NotificationType)classifyNotification:(UNNotification*)notification {
  // Use an arbitrary profile that is loaded - a specific profile is not needed
  // to determine the type of notification.
  std::vector<ProfileIOS*> loaded_profiles =
      GetApplicationContext()->GetProfileManager()->GetLoadedProfiles();
  CHECK(!loaded_profiles.empty());
  ProfileIOS* profile = loaded_profiles.back();
  PushNotificationClient* client = [self clientForNotification:notification
                                                       profile:profile];
  if (!client) {
    return NotificationType::kUnknown;
  }
  std::optional<NotificationType> type =
      client->GetNotificationType(notification);
  return type.value_or(NotificationType::kUnknown);
}

#pragma mark - AppStateObserver

- (void)appState:(AppState*)appState
    sceneDidBecomeActive:(SceneState*)sceneState {
  if (sceneState.profileState.initStage < ProfileInitStage::kFinal) {
    [sceneState.profileState addObserver:self];
    return;
  }

  if (sceneState.activationLevel < SceneActivationLevelForegroundActive) {
    return;
  }

  [self appDidEnterForeground:sceneState];
}

- (void)appState:(AppState*)appState sceneConnected:(SceneState*)sceneState {
  [sceneState addObserver:self];
  [self sceneState:sceneState
      transitionedToActivationLevel:sceneState.activationLevel];
}

#pragma mark - ProfileStateObserver

- (void)profileState:(ProfileState*)profileState
    didTransitionToInitStage:(ProfileInitStage)nextInitStage
               fromInitStage:(ProfileInitStage)fromInitStage {
  if (nextInitStage < ProfileInitStage::kFinal) {
    return;
  }

  for (SceneState* sceneState in profileState.connectedScenes) {
    if (sceneState.activationLevel < SceneActivationLevelForegroundActive) {
      continue;
    }

    [self appDidEnterForeground:sceneState];
  }

  [profileState removeObserver:self];
}

#pragma mark - SceneStateObserver

- (void)sceneState:(SceneState*)sceneState
    transitionedToActivationLevel:(SceneActivationLevel)level {
  if (level < SceneActivationLevelForegroundActive) {
    return;
  }

  if (sceneState.profileState.initStage < ProfileInitStage::kFinal) {
    [sceneState.profileState addObserver:self];
    return;
  }

  [self appDidEnterForeground:sceneState];
}

- (void)sceneState:(SceneState*)sceneState
    profileStateConnected:(ProfileState*)profileState {
  [profileState addObserver:self];
}

#pragma mark - Property accessors

- (SceneState*)foregroundActiveScene {
  for (SceneState* sceneState in _appState.connectedScenes) {
    if (sceneState.activationLevel < SceneActivationLevelForegroundActive) {
      continue;
    }

    if (sceneState.profileState.initStage < ProfileInitStage::kFinal) {
      continue;
    }

    return sceneState;
  }

  return nil;
}

- (PushNotificationClientManager*)appWideClientManager {
  return GetApplicationContext()
      ->GetPushNotificationService()
      ->GetPushNotificationClientManager();
}

#pragma mark - Private

// Handles a notification that is about to be presented.
- (void)handleWillPresentNotification:(UNNotification*)notification
                withCompletionHandler:
                    (void (^)(UNNotificationPresentationOptions options))
                        completionHandler {
  [self.metricsRecorder recordReceived:notification];
  [self recordLifeCycleEvent:PushNotificationLifecycleEvent::
                                 kNotificationForegroundPresentation];

  NSDictionary* userInfo = notification.request.content.userInfo;
  __weak __typeof(self) weakSelf = self;
  void (^presentationCompletionBlock)(UIBackgroundFetchResult result) =
      ^(UIBackgroundFetchResult /* result */) {
        [weakSelf handlePresentationCompletionWithUserInfo:userInfo
                                         completionHandler:completionHandler];
      };

  if (IsMultiProfilePushNotificationHandlingEnabled()) {
    ProcessIncomingNotification(
        userInfo,
        PushNotificationClientManagerFailurePoint::kWillPresentNotification,
        presentationCompletionBlock);
  } else {
    HandleNotificationReceptionWithAppWideManager(userInfo,
                                                  presentationCompletionBlock);
  }
}

// Determines how a notification should be presented when received while the app
// is in the foreground and invokes the system completion handler with the
// appropriate options. It also logs a histogram for the notification event.
- (void)handlePresentationCompletionWithUserInfo:(NSDictionary*)userInfo
                               completionHandler:
                                   (void (^)(UNNotificationPresentationOptions
                                                 options))completionHandler {
  if (completionHandler) {
    BOOL isSendTab = ([userInfo[kPushNotificationClientIdKey] intValue] ==
                      static_cast<int>(PushNotificationClientId::kSendTab));
    BOOL isForeground = (self.foregroundActiveScene != nil);

    UNNotificationPresentationOptions presentationOptions =
        (isSendTab && isForeground) ? UNNotificationPresentationOptionNone
                                    : UNNotificationPresentationOptionBanner;

    // TODO(crbug.com/408085973): Add PushNotificationDelegate unittest suite.
    // Cover critical paths and error cases.
    completionHandler(presentationOptions);
  }

  base::UmaHistogramEnumeration(kAppLaunchSource,
                                AppLaunchSource::NOTIFICATION);
}

// Executes blocks queued in _runAfterForeground. If multi-profile handling is
// enabled, also notifies the profile-specific PushNotificationClientManager
// for the given sceneState upon readiness.
- (void)handleQueuedBlocksWithSceneState:(SceneState*)sceneState {
  NSMutableArray<ProceduralBlock>* blocks = _runAfterForeground;
  _runAfterForeground = nil;

  for (ProceduralBlock block in blocks) {
    block();
  }

  if (IsMultiProfilePushNotificationHandlingEnabled()) {
    if (!sceneState || !sceneState.profileState.profile) {
      return;
    }

    PushNotificationClientManager* clientManager =
        GetClientManagerForProfile(sceneState.profileState.profile);

    if (!clientManager) {
      RecordClientManagerAccessFailure(
          PushNotificationClientManagerFailurePoint::kAppDidEnterForeground);
      return;
    }

    clientManager->OnSceneActiveForegroundBrowserReady();
  }
}

// Notifies the client manager that the scene is "foreground active".
- (void)appDidEnterForeground:(SceneState*)sceneState {
  PushNotificationClientManager* appWideClientManager =
      self.appWideClientManager;
  DCHECK(appWideClientManager);
  appWideClientManager->OnSceneActiveForegroundBrowserReady();
  [self.metricsRecorder
      handleDeliveredNotificationsWithClosure:base::DoNothing()];

  __weak PushNotificationDelegate* weakSelf = self;
  __weak SceneState* weakSceneState = sceneState;

  // Asynchronously processes any queued `_runAfterForeground` blocks.
  //
  // This is crucial for notification interactions (queued via
  // `-executeWhenForeground:`), as handling them might require tearing down the
  // current Browser/Profile to switch Profiles. Asynchronous processing ensures
  // that all current observers (potentially observing the Browser/Profile being
  // torn down) finish their work *before* these objects are destroyed. This
  // prevents state modification or destruction while observers are still active
  // in the original synchronous call stack.
  dispatch_async(dispatch_get_main_queue(), ^{
    [weakSelf handleQueuedBlocksWithSceneState:weakSceneState];
  });

  ProfileIOS* profile = sceneState.profileState.profile;
  if (IsContentNotificationEnabled(profile)) {
    ContentNotificationService* contentNotificationService =
        ContentNotificationServiceFactory::GetForProfile(profile);
    int maxNauSentPerSession = kDeliveredNAUMaxPerSessionFeature.Get();
    // Check if there are notifications received in the background to send the
    // respective NAUs.
    NSUserDefaults* defaults = app_group::GetGroupUserDefaults();
    if ([defaults objectForKey:kContentNotificationContentArrayKey] != nil) {
      NSMutableArray* contentArray = [[defaults
          objectForKey:kContentNotificationContentArrayKey] mutableCopy];
      // Report in 5 item increments.
      NSMutableArray* uploadedItems = [NSMutableArray array];
      for (NSData* item in contentArray) {
        ContentNotificationNAUConfiguration* config =
            [[ContentNotificationNAUConfiguration alloc] init];
        config.actionType = NAUActionTypeDisplayed;
        UNNotificationContent* content = [NSKeyedUnarchiver
            unarchivedObjectOfClass:UNMutableNotificationContent.class
                           fromData:item
                              error:nil];
        config.content = content;
        contentNotificationService->SendNAUForConfiguration(config);
        [uploadedItems addObject:item];
        base::UmaHistogramEnumeration(
            kContentNotificationActionHistogramName,
            NotificationActionType::kNotificationActionTypeDisplayed);
        if ((int)uploadedItems.count == maxNauSentPerSession) {
          break;
        }
      }
      [contentArray removeObjectsInArray:uploadedItems];
      if (contentArray.count > 0) {
        [defaults setObject:contentArray
                     forKey:kContentNotificationContentArrayKey];
      } else {
        [defaults setObject:nil forKey:kContentNotificationContentArrayKey];
      }
    }
    // Send an NAU on every foreground to report the OS Auth Settings.
    [self sendSettingsChangeNAUForProfile:profile];
  }
  [PushNotificationUtil updateAuthorizationStatusPref];

  // For Reactivation Notifications, ask for provisional auth right away.
  if (IsFirstRunRecent(base::Days(28)) &&
      IsIOSReactivationNotificationsEnabled()) {
    UNAuthorizationStatus auth_status =
        [PushNotificationUtil getSavedPermissionSettings];
    if (auth_status == UNAuthorizationStatusNotDetermined) {
      [PushNotificationUtil enableProvisionalPushNotificationPermission:nil];
    }
  }
}

- (void)sendSettingsChangeNAUForProfile:(ProfileIOS*)profile {
  [PushNotificationUtil
      getPermissionSettings:base::CallbackToBlock(base::BindOnce(
                                &SendNAUFConfigurationForProfileWithSettings,
                                profile->AsWeakPtr()))];
}

- (void)recordLifeCycleEvent:(PushNotificationLifecycleEvent)event {
  base::UmaHistogramEnumeration(kLifecycleEventsHistogram, event);
}

- (BOOL)isContentNotificationAvailable:(ProfileIOS*)profile {
  return IsContentNotificationEnabled(profile) ||
         IsContentNotificationRegistered(profile);
}

// If user has not previously disabled Send Tab notifications, either 1) If user
// has authorized full notification permissions, enables Send Tab notifications
// OR 2) enrolls user in provisional notifications for Send Tab notification
// type.
- (void)setUpAndEnableSendTabNotificationsWithProfile:(ProfileIOS*)profile {
  // Refresh the local device info now that the client has a Chime
  // Representative Target ID.
  syncer::DeviceInfoSyncService* deviceInfoSyncService =
      DeviceInfoSyncServiceFactory::GetForProfile(profile);
  deviceInfoSyncService->RefreshLocalDeviceInfo();

  AuthenticationService* authService =
      AuthenticationServiceFactory::GetForProfile(profile);
  GaiaId gaiaID = authService->GetPrimaryIdentity().gaiaId;

  // Early return if 1) the user has previously disabled Send Tab push
  // notifications, because in that case we don't want to automatically enable
  // the notification type or 2) if Send Tab notifications are already enabled.
  if (profile->GetPrefs()->GetBoolean(
          prefs::kSendTabNotificationsPreviouslyDisabled) ||
      push_notification_settings::
          GetMobileNotificationPermissionStatusForClient(
              PushNotificationClientId::kSendTab, gaiaID)) {
    return;
  }

  if ([PushNotificationUtil getSavedPermissionSettings] ==
      UNAuthorizationStatusAuthorized) {
    GetApplicationContext()->GetPushNotificationService()->SetPreference(
        gaiaID, PushNotificationClientId::kSendTab, true);
  } else {
    ProvisionalPushNotificationServiceFactory::GetForProfile(profile)
        ->EnrollUserToProvisionalNotifications(
            ProvisionalPushNotificationService::ClientIdState::kEnabled,
            {PushNotificationClientId::kSendTab});
  }
}

// Runs the given `block` immediately if the app has an active foreground
// scene connected, otherwise stores it to be called when the app is
// foregrounded.
- (void)executeWhenForeground:(ProceduralBlock)block {
  if (self.foregroundActiveScene) {
    block();
    return;
  }

  if (!_runAfterForeground) {
    _runAfterForeground = [[NSMutableArray alloc] init];
  }
  [_runAfterForeground addObject:block];
}

// Handles a notification response by sending it to the push notification
// client manager.
- (void)handleNotificationResponse:(UNNotificationResponse*)response {
  DCHECK_GE(_appState.initStage,
            AppInitStage::kBrowserObjectsForBackgroundHandlers);

  if (IsMultiProfilePushNotificationHandlingEnabled()) {
    [self handleProfileSpecificNotificationResponse:response];
    return;
  }

  // Notifications are intentionally passed on to the `appWideClientManager`
  // even when the profile specific one handles them. In a future refacor, if a
  // notification is properly handled in a profile specific manager it likely
  // should not be passed onto the app wide manager.

  self.appWideClientManager->HandleNotificationInteraction(response);
}

// Handles a notification interaction specifically for the multi-Profile case.
//
// It determines the Profile the notification originated from and the target
// scene where the interaction should occur. Based on whether the target scene's
// current Profile matches the notification's originating Profile, it either:
//
// 1. Handles the interaction directly using the current context if the Profiles
//    match.
// 2. Initiates a Profile switch for the target scene if the Profiles do not
//    match. A continuation callback is provided to the switching mechanism to
//    process the interaction once the correct Profile is active.
//
// Failures encountered during Profile validation, scene lookup, manager
// retrieval, or switch initiation are logged to UMA for monitoring.
- (void)handleProfileSpecificNotificationResponse:
    (UNNotificationResponse*)response {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  std::string profileName = GetProfileNameFromUserInfo(
      response.notification.request.content.userInfo);

  if (profileName.empty()) {
    // No profile name was found, so allow app-wide clients the opportunity to
    // handle interactions.
    self.appWideClientManager->HandleNotificationInteraction(response);

    RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
                                         kHandleInteractionInvalidProfileName);

    RecordPushNotificationTargetProfileHandlingResult(
        PushNotificationTargetProfileHandlingResult::kProfileUnidentifiable);

    return;
  }

  SceneState* targetSceneState =
      [self notificationTargetSceneStateForResponse:response];

  if (!targetSceneState) {
    RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
                                         kHandleInteractionMissingTargetScene);

    targetSceneState = self.foregroundActiveScene;
  }

  if (!targetSceneState) {
    RecordClientManagerAccessFailure(
        PushNotificationClientManagerFailurePoint::
            kHandleInteractionMissingFallbackScene);

    RecordPushNotificationTargetProfileHandlingResult(
        PushNotificationTargetProfileHandlingResult::kFailureSceneUnavailable);

    return;
  }

  ProfileIOS* sceneProfile = targetSceneState.profileState.profile;

  if (!sceneProfile || profileName != sceneProfile->GetProfileName()) {
    RecordPushNotificationTargetProfileHandlingResult(
        PushNotificationTargetProfileHandlingResult::
            kSwitchEnsuredCorrectProfile);
  } else {
    RecordPushNotificationTargetProfileHandlingResult(
        PushNotificationTargetProfileHandlingResult::kCorrectProfileActive);
  }

  id<ChangeProfileCommands> handler =
      HandlerForProtocol(_appState.appCommandDispatcher, ChangeProfileCommands);

  CHECK(handler);

  [handler changeProfile:profileName
                forScene:targetSceneState
                  reason:ChangeProfileReason::kHandlePushNotification
            continuation:CreateNotificationInteractionContinuation(response)];
}

// Shows the app's notification settings, switching to the given `profileName`
// profile if needed.
- (void)openSettingsForNotification:(UNNotification*)notification
                        profileName:(std::string_view)profileName {
  SceneState* sceneState = self.foregroundActiveScene;
  CHECK(sceneState);
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  id<ChangeProfileCommands> handler =
      HandlerForProtocol(_appState.appCommandDispatcher, ChangeProfileCommands);

  __weak __typeof(self) weakSelf = self;
  ChangeProfileContinuation continuation = base::BindOnce(
      [](__typeof(self) strong_self, UNNotification* notification,
         SceneState* new_scene_state, base::OnceClosure completion_closure) {
        [strong_self openSettingsForNotification:notification
                                           scene:new_scene_state
                                      completion:std::move(completion_closure)];
      },
      weakSelf, notification);

  [handler changeProfile:profileName
                forScene:sceneState
                  reason:ChangeProfileReason::kHandlePushNotification
            continuation:std::move(continuation)];
}

// Shows the app's notification settings in the given `sceneState`, and calls
// `completion` when finished.
- (void)openSettingsForNotification:(UNNotification*)notification
                              scene:(SceneState*)sceneState
                         completion:(base::OnceClosure)completion {
  CHECK(sceneState);
  Browser* browser =
      sceneState.browserProviderInterface.mainBrowserProvider.browser;
  ProfileIOS* profile = browser->GetProfile();

  // Get the clientID for the client that is associated with this notification.
  PushNotificationClient* client = [self clientForNotification:notification
                                                       profile:profile];
  std::optional<PushNotificationClientId> clientID =
      (client) ? std::make_optional(client->GetClientId()) : std::nullopt;

  CommandDispatcher* dispatcher = browser->GetCommandDispatcher();
  id<SceneCommands> sceneHandler =
      HandlerForProtocol(dispatcher, SceneCommands);
  id<SettingsCommands> settingsHandler =
      HandlerForProtocol(dispatcher, SettingsCommands);
  __block base::OnceClosure completion2 = std::move(completion);
  [sceneHandler
      prepareToPresentModalWithSnackbarDismissal:YES
                                      completion:^{
                                        [settingsHandler
                                            showNotificationsSettingsAndHighlightClient:
                                                clientID];
                                        std::move(completion2).Run();
                                      }];
}

// Returns the client to handle the given `notification`. The client can be
// either profile-scoped (and associated with the given `profile`), or
// app-scoped.
- (PushNotificationClient*)clientForNotification:(UNNotification*)notification
                                         profile:(ProfileIOS*)profile {
  if (notification == nil) {
    return nullptr;
  }
  PushNotificationClient* client = nullptr;
  if (IsMultiProfilePushNotificationHandlingEnabled()) {
    PushNotificationClientManager* clientManager =
        GetClientManagerForProfile(profile);
    client = clientManager->GetClientForNotification(notification);
  }
  if (!client) {
    client = self.appWideClientManager->GetClientForNotification(notification);
  }
  return client;
}

// Returns the `SceneState` matching the notification response's target scene,
// if any.
- (SceneState*)notificationTargetSceneStateForResponse:
    (UNNotificationResponse*)response {
  UIScene* targetScene = response.targetScene;

  if (!targetScene) {
    RecordClientManagerAccessFailure(
        PushNotificationClientManagerFailurePoint::kGetResponseTargetSceneNil);

    return nil;
  }
  for (SceneState* sceneState in _appState.connectedScenes) {
    if (sceneState.scene == targetScene) {
      return sceneState;
    }
  }

  // No matching scene found
  return nil;
}

@end
