// 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_client.h"

#import "base/functional/callback_helpers.h"
#import "base/metrics/histogram_functions.h"
#import "base/notreached.h"
#import "base/strings/strcat.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/push_notification/model/constants.h"
#import "ios/chrome/browser/push_notification/model/push_notification_prefs.h"
#import "ios/chrome/browser/safety_check_notifications/utils/constants.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.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/profile/features.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.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/tips_notifications/model/utils.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/public/provider/chrome/browser/user_feedback/user_feedback_sender.h"

namespace {

// Logs a failure reason to the appropriate patterned histogram based on
// `client_id` and `reason`.
void LogProfileRequestCreationFailure(
    PushNotificationClientId client_id,
    ProfileNotificationRequestCreationFailureReason reason) {
  std::string client_name = PushNotificationClientIdToString(client_id);

  std::string histogram_name =
      base::StrCat({"IOS.PushNotification.ProfileRequestCreationFailureReason.",
                    client_name});

  base::UmaHistogramEnumeration(histogram_name, reason);
}

// Constant string for the error domain related to Profile-based local
// notifications.
const NSErrorDomain kIOSProfileLocalNotificationErrorDomain =
    @"ios_profile_local_notification_error_domain";

// `NSError` error codes specifically for Profile-based iOS notification
// handling.
enum class IOSProfileLocalNotificationErrorCode {
  // Indicates that Profile-based notification scheduling failed due to an
  // invalid or missing Profile.
  kInvalidProfile = 1,
  // Indicates that the `UNNotificationRequest` could not be created for a
  // Profile-based notification.
  kRequestCreationFailed = 2,
};

// Creates a standardized `NSError` for Profile-based notification scheduling
// failures due to an invalid or missing Profile.
NSError* CreateInvalidProfileError() {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  NSDictionary* user_info = @{
    NSLocalizedDescriptionKey : @"Invalid Profile provided when scheduling "
                                @"Profile-based local notification.",
  };

  return [NSError
      errorWithDomain:kIOSProfileLocalNotificationErrorDomain
                 code:static_cast<NSInteger>(
                          IOSProfileLocalNotificationErrorCode::kInvalidProfile)
             userInfo:user_info];
}

// Creates a standardized `NSError` for failures to create a
// `UNNotificationRequest` for a Profile-based notification.
NSError* CreateRequestCreationError() {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  NSDictionary* userInfo = @{
    NSLocalizedDescriptionKey : @"Failed to create the UNNotificationRequest "
                                @"for Profile-based local notification.",
  };

  return [NSError errorWithDomain:kIOSProfileLocalNotificationErrorDomain
                             code:static_cast<NSInteger>(
                                      IOSProfileLocalNotificationErrorCode::
                                          kRequestCreationFailed)
                         userInfo:userInfo];
}

// Helper function to add the original Profile name to a Profile-based
// notification content's `userInfo` dictionary.
void AddProfileNameToNotificationContent(UNMutableNotificationContent* content,
                                         std::string_view profile_name) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());
  CHECK(content);
  CHECK(!profile_name.empty());

  NSMutableDictionary* mutable_user_info =
      [content.userInfo mutableCopy] ?: [NSMutableDictionary dictionary];

  std::string name = std::string(profile_name);

  mutable_user_info[kOriginatingProfileNameKey] = base::SysUTF8ToNSString(name);

  content.userInfo = mutable_user_info;
}

// Searches for a browser associated with the provided `profile`. Returns the
// first matching browser with `SceneActivationLevelForegroundActive`, or
// `nullptr` if none exists for this `profile`.
Browser* GetSceneLevelForegroundActiveBrowserForProfile(ProfileIOS* profile) {
  if (!profile) {
    return nullptr;
  }

  BrowserList* browser_list = BrowserListFactory::GetForProfile(profile);

  if (!browser_list) {
    return nullptr;
  }

  std::set<Browser*> browsers =
      browser_list->BrowsersOfType(BrowserList::BrowserType::kRegular);

  for (Browser* browser : browsers) {
    if (!browser) {
      continue;
    }

    if (browser->GetSceneState().activationLevel ==
        SceneActivationLevelForegroundActive) {
      return browser;
    }
  }

  return nullptr;
}

}  // namespace

PushNotificationClient::PushNotificationClient(
    PushNotificationClientId client_id,
    ProfileIOS* profile)
    : client_id_(client_id),
      client_scope_(PushNotificationClientScope::kPerProfile),
      profile_(profile->AsWeakPtr()) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());
  CHECK(profile_.get())
      << "Profile must be provided for kPerProfile client "
         "when IsMultiProfilePushNotificationHandlingEnabled() returns YES";
  // Ensure this Profile is not an off-the-record Profile.
  // Off-the-record (incognito) Profiles have an empty Profile name.
  CHECK(!profile->GetProfileName().empty())
      << "Expected a regular Profile, but GetProfileName() is empty, "
      << "indicating an off-the-record Profile.";
  CHECK(!profile->IsOffTheRecord()) << "Notifications are not supported for "
                                       "off-the-record (incognito) Profiles.";
}

PushNotificationClient::PushNotificationClient(
    PushNotificationClientId client_id,
    PushNotificationClientScope client_scope)
    : client_id_(client_id), client_scope_(client_scope) {}

PushNotificationClient::~PushNotificationClient() = default;

std::optional<NotificationType> PushNotificationClient::GetNotificationType(
    UNNotification* notification) {
  return std::nullopt;
}

PushNotificationClientId PushNotificationClient::GetClientId() const {
  return client_id_;
}

PushNotificationClientScope PushNotificationClient::GetClientScope() const {
  return client_scope_;
}

void PushNotificationClient::OnSceneActiveForegroundBrowserReady() {
  if (actions_delayed_for_loading_.empty() && !feedback_presentation_delayed_) {
    return;
  }

  // TODO(crbug.com/41497027): The notifications should probbaly be linked
  // to a specific profile, and thus this should check that the code here
  // use the correct profile.
  Browser* browser = GetActiveForegroundBrowser();
  CHECK(browser);

  if (feedback_presentation_delayed_) {
    id<SceneCommands> handler =
        HandlerForProtocol(browser->GetCommandDispatcher(), SceneCommands);
    switch (feedback_presentation_delayed_client_) {
      case PushNotificationClientId::kContent:
      case PushNotificationClientId::kSports:
        [handler
            showReportAnIssueFromViewController:browser->GetSceneState()
                                                    .window.rootViewController
                                         sender:UserFeedbackSender::
                                                    ContentNotification
                            specificProductData:feedback_data_];
        feedback_presentation_delayed_ = false;
        break;
      case PushNotificationClientId::kTips:
      case PushNotificationClientId::kCommerce:
      case PushNotificationClientId::kSendTab:
      case PushNotificationClientId::kSafetyCheck:
      case PushNotificationClientId::kReminders:
      case PushNotificationClientId::kCrossPlatformPromos:
        // Features do not support feedback.
        NOTREACHED();
    }
  }

  actions_delayed_for_loading_.Notify(browser);
}

Browser* PushNotificationClient::GetActiveForegroundBrowser() const {
  if (!IsMultiProfilePushNotificationHandlingEnabled() ||
      client_scope_ != PushNotificationClientScope::kPerProfile) {
    for (ProfileIOS* profile :
         GetApplicationContext()->GetProfileManager()->GetLoadedProfiles()) {
      if (Browser* browser =
              GetSceneLevelForegroundActiveBrowserForProfile(profile)) {
        return browser;
      }
    }

    return nullptr;
  }

  return GetSceneLevelForegroundActiveBrowserForProfile(profile_.get());
}

ProfileIOS* PushNotificationClient::GetProfile() const {
  CHECK_EQ(client_scope_, PushNotificationClientScope::kPerProfile);

  return profile_.get();
}

base::CallbackListSubscription
PushNotificationClient::ExecuteActionWhenBrowserReady(
    base::OnceCallback<void(Browser*)> action) {
  Browser* browser = GetActiveForegroundBrowser();
  if (!browser) {
    return actions_delayed_for_loading_.Add(std::move(action));
  }
  std::move(action).Run(browser);
  return {};
}

void PushNotificationClient::LoadUrlInNewTab(const GURL& url) {
  LoadUrlInNewTab(url, base::DoNothing());
}

void PushNotificationClient::LoadUrlInNewTab(
    const GURL& url,
    base::OnceCallback<void(Browser*)> callback) {
  if (base::CallbackListSubscription subscription =
          ExecuteActionWhenBrowserReady(base::BindOnce(
              &PushNotificationClient::ExecuteLoadUrlInNewTab,
              weak_ptr_factory_.GetWeakPtr(), url, std::move(callback)))) {
    delayed_url_subscriptions_.push_back(std::move(subscription));
  }
}

void PushNotificationClient::ExecuteLoadUrlInNewTab(
    const GURL& url,
    base::OnceCallback<void(Browser*)> callback,
    Browser* browser) {
  id<SceneCommands> handler =
      HandlerForProtocol(browser->GetCommandDispatcher(), SceneCommands);
  [handler openURLInNewTab:[OpenNewTabCommand commandWithURLFromChrome:url]];
  std::move(callback).Run(browser);
}

void PushNotificationClient::LoadFeedbackWithPayloadAndClientId(
    NSDictionary<NSString*, NSString*>* data,
    PushNotificationClientId client) {
  Browser* browser = GetActiveForegroundBrowser();
  if (!browser && data) {
    feedback_presentation_delayed_client_ = client;
    feedback_presentation_delayed_ = true;
    feedback_data_ = data;
    return;
  }
}

void PushNotificationClient::ScheduleProfileNotification(
    ScheduledNotificationRequest request,
    base::OnceCallback<void(NSError*)> completion,
    std::string_view profile_name) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  if (profile_name.empty()) {
    std::move(completion).Run(CreateInvalidProfileError());

    return;
  }

  UNNotificationRequest* notification_request =
      CreateRequestForProfile(request, profile_name);

  if (!notification_request) {
    std::move(completion).Run(CreateRequestCreationError());

    return;
  }

  auto completion_block = base::CallbackToBlock(std::move(completion));

  [UNUserNotificationCenter.currentNotificationCenter
      addNotificationRequest:notification_request
       withCompletionHandler:completion_block];
}

void PushNotificationClient::CheckRateLimitBeforeSchedulingNotification(
    ScheduledNotificationRequest request,
    base::OnceCallback<void(NSError*)> completion) {
  base::Time last_send_tab_open =
      GetApplicationContext()->GetLocalState()->GetTime(
          push_notification_prefs::kSendTabLastOpenTimestamp);
  const base::TimeDelta time_since_open =
      base::Time::Now() - last_send_tab_open;
  if (time_since_open < base::Minutes(10)) {
    // Delay the notification if there was a Send Tab To Self Notification
    // delivered in the last 10 minutes.
    request.time_interval += base::Days(1);
    ScheduleNotification(request, std::move(completion));
    return;
  }

  auto completion_handler = base::CallbackToBlock(base::BindPostTask(
      base::SequencedTaskRunner::GetCurrentDefault(),
      base::BindOnce(&PushNotificationClient::HandlePendingNotificationResult,
                     weak_ptr_factory_.GetWeakPtr(), std::move(request),
                     std::move(completion))));

  [UNUserNotificationCenter.currentNotificationCenter
      getPendingNotificationRequestsWithCompletionHandler:completion_handler];
}

std::optional<ForcedNotificationPayload>
PushNotificationClient::BuildForcedNotificationPayload(
    int subtype,
    NSMutableDictionary* user_info) {
  return std::nullopt;
}

void PushNotificationClient::HandlePendingNotificationResult(
    ScheduledNotificationRequest notification,
    base::OnceCallback<void(NSError*)> completion,
    NSArray<UNNotificationRequest*>* requests) {
  if ([requests count] > 0) {
    // Delay a tips notification if there is a scheduled Safety Check
    // notification.
    NSArray* safetyCheckIds = @[
      kSafetyCheckSafeBrowsingNotificationID,
      kSafetyCheckUpdateChromeNotificationID,
      kSafetyCheckPasswordNotificationID,
    ];
    for (UNNotificationRequest* request in requests) {
      if ([notification.identifier isEqualToString:kTipsNotificationId]) {
        if ([safetyCheckIds containsObject:request.identifier]) {
          notification.time_interval += base::Days(1);
          break;
        }
      }
    }
  }
  ScheduleNotification(notification, std::move(completion));
}

void PushNotificationClient::ScheduleNotification(
    ScheduledNotificationRequest request,
    base::OnceCallback<void(NSError*)> completion) {
  auto completion_block = base::CallbackToBlock(std::move(completion));

  [UNUserNotificationCenter.currentNotificationCenter
      addNotificationRequest:CreateRequest(request)
       withCompletionHandler:completion_block];
}

UNNotificationRequest* PushNotificationClient::CreateRequest(
    ScheduledNotificationRequest request) {
  if ([request.identifier isEqualToString:kTipsNotificationId]) {
    return [UNNotificationRequest
        requestWithIdentifier:kTipsNotificationId
                      content:request.content
                      trigger:[UNTimeIntervalNotificationTrigger
                                  triggerWithTimeInterval:request.time_interval
                                                              .InSecondsF()
                                                  repeats:NO]];
  }
  NOTREACHED();
}

UNNotificationRequest* PushNotificationClient::CreateRequestForProfile(
    ScheduledNotificationRequest request,
    std::string_view profile_name) {
  CHECK(IsMultiProfilePushNotificationHandlingEnabled());

  if (profile_name.empty()) {
    LogProfileRequestCreationFailure(
        client_id_,
        ProfileNotificationRequestCreationFailureReason::kInvalidProfileName);
    return nil;
  }

  if (!request.time_interval.is_positive()) {
    LogProfileRequestCreationFailure(
        client_id_,
        ProfileNotificationRequestCreationFailureReason::kInvalidTimeInterval);
    return nil;
  }

  if (!request.identifier || request.identifier.length == 0) {
    LogProfileRequestCreationFailure(
        client_id_,
        ProfileNotificationRequestCreationFailureReason::kInvalidIdentifier);
    return nil;
  }

  if (!request.content) {
    LogProfileRequestCreationFailure(
        client_id_,
        ProfileNotificationRequestCreationFailureReason::kInvalidSourceContent);
    return nil;
  }

  UNMutableNotificationContent* mutable_content = [request.content mutableCopy];

  if (!mutable_content) {
    LogProfileRequestCreationFailure(
        client_id_,
        ProfileNotificationRequestCreationFailureReason::kContentCopyFailed);
    return nil;
  }

  AddProfileNameToNotificationContent(mutable_content, profile_name);

  UNNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger
      triggerWithTimeInterval:request.time_interval.InSecondsF()
                      repeats:NO];

  CHECK(trigger);

  return [UNNotificationRequest requestWithIdentifier:request.identifier
                                              content:mutable_content
                                              trigger:trigger];
}
