// Copyright (c) 2015 Felix Rieseberg <feriese@microsoft.com> and Jason Poon
// <jason.poon@microsoft.com>. All rights reserved.
// Copyright (c) 2015 Ryan McShane <rmcshane@bandwidth.com> and Brandon Smith
// <bsmith@bandwidth.com>
// Thanks to both of those folks mentioned above who first thought up a bunch of
// this code
// and released it as MIT to the world.

#include "shell/browser/notifications/win/windows_toast_notification.h"

#include <string_view>

#include <shlobj.h>
#include <wrl\wrappers\corewrappers.h>

#include "base/containers/fixed_flat_map.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/string_util_win.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "shell/browser/notifications/notification_delegate.h"
#include "shell/browser/notifications/win/notification_presenter_win.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include "shell/browser/win/scoped_hstring.h"
#include "shell/common/application_info.h"
#include "third_party/libxml/chromium/xml_writer.h"
#include "ui/base/l10n/l10n_util_win.h"
#include "ui/strings/grit/ui_strings.h"

using ABI::Windows::Data::Xml::Dom::IXmlDocument;
using ABI::Windows::Data::Xml::Dom::IXmlDocumentIO;
using ABI::Windows::Foundation::IPropertyValue;
using ABI::Windows::Foundation::Collections::IIterable;
using ABI::Windows::Foundation::Collections::IIterator;
using ABI::Windows::Foundation::Collections::IKeyValuePair;
using ABI::Windows::Foundation::Collections::IMap;
using ABI::Windows::Foundation::Collections::IPropertySet;
using Microsoft::WRL::Wrappers::HStringReference;

namespace winui = ABI::Windows::UI;

#define RETURN_IF_FAILED(hr)                           \
  do {                                                 \
    if (const HRESULT _hrTemp = hr; FAILED(_hrTemp)) { \
      return _hrTemp;                                  \
    }                                                  \
  } while (false)

#define REPORT_AND_RETURN_IF_FAILED(hr, msg)                               \
  do {                                                                     \
    if (const HRESULT _hrTemp = hr; FAILED(_hrTemp)) {                     \
      std::string _err =                                                   \
          base::StrCat({msg, ", ERROR ", FailureResultToString(_hrTemp)}); \
      DebugLog(_err);                                                      \
      Notification::NotificationFailed(_err);                              \
      return _hrTemp;                                                      \
    }                                                                      \
  } while (false)

namespace electron {

namespace {

// This string needs to be max 16 characters to work on Windows 10 prior to
// applying Creators Update (build 15063).
constexpr wchar_t kGroup[] = L"Notifications";

void DebugLog(std::string_view log_msg) {
  if (electron::debug_notifications)
    LOG(INFO) << log_msg;
}

std::wstring GetTag(const std::string_view notification_id) {
  return base::UTF8ToWide(notification_id);
}

// See https://www.hresult.info for HRESULT error codes.
const std::string FailureResultToString(HRESULT failure_reason) {
  static constexpr auto kFailureMessages = base::MakeFixedFlatMap<
      HRESULT, std::string_view>(
      {{-2143420143, "Settings prevent the notification from being delivered."},
       {-2143420142,
        "Application capabilities prevent the notification from being "
        "delivered."},
       {-2143420140,
        "Settings prevent the notification type from being delivered."},
       {-2143420139, "The size of the notification content is too large."},
       {-2143420138, "The size of the notification tag is too large."},
       {-2143420155, "The notification platform is unavailable."},
       {-2143420154, "The notification has already been posted."},
       {-2143420153, "The notification has already been hidden."},
       {-2143420128,
        "The size of the developer id for scheduled notification is too "
        "large."},
       {-2143420118, "The notification tag is not alphanumeric."},
       {-2143419897,
        "Toast Notification was dropped without being displayed to the user."},
       {-2143419896,
        "The notification platform does not have the proper privileges to "
        "complete the request."}});

  std::string hresult_str = base::StrCat(
      {" (HRESULT: ", base::NumberToString(static_cast<long>(failure_reason)),
       ")"});

  if (const auto it = kFailureMessages.find(failure_reason);
      it != kFailureMessages.end()) {
    return base::StrCat({"Notification failed - ", it->second, hresult_str});
  }

  return hresult_str;
}

constexpr char kPlaceholderContent[] = "placeHolderContent";
constexpr char kContent[] = "content";
constexpr char kToast[] = "toast";
constexpr char kVisual[] = "visual";
constexpr char kBinding[] = "binding";
constexpr char kTemplate[] = "template";
constexpr char kToastTemplate[] = "ToastGeneric";
constexpr char kText[] = "text";
constexpr char kImage[] = "image";
constexpr char kPlacement[] = "placement";
constexpr char kAppLogoOverride[] = "appLogoOverride";
constexpr char kHintCrop[] = "hint-crop";
constexpr char kHintInputId[] = "hint-inputId";
constexpr char kHintCropNone[] = "none";
constexpr char kSrc[] = "src";
constexpr char kAudio[] = "audio";
constexpr char kSilent[] = "silent";
constexpr char kReply[] = "reply";
constexpr char kTrue[] = "true";
constexpr char kID[] = "id";
constexpr char kInput[] = "input";
constexpr char kType[] = "type";
constexpr char kSelection[] = "selection";
constexpr char kScenario[] = "scenario";
constexpr char kReminder[] = "reminder";
constexpr char kActions[] = "actions";
constexpr char kAction[] = "action";
constexpr char kActivationType[] = "activationType";
constexpr char kActivationTypeForeground[] = "foreground";
constexpr char kActivationTypeSystem[] = "system";
constexpr char kArguments[] = "arguments";
constexpr char kDismiss[] = "dismiss";
// The XML version header that has to be stripped from the output.
constexpr char kXmlVersionHeader[] = "<?xml version=\"1.0\"?>\n";

}  // namespace

// static
ComPtr<winui::Notifications::IToastNotificationManagerStatics>*
    WindowsToastNotification::toast_manager_ = nullptr;

// static
ComPtr<winui::Notifications::IToastNotifier>*
    WindowsToastNotification::toast_notifier_ = nullptr;

// static
scoped_refptr<base::SequencedTaskRunner>
WindowsToastNotification::GetToastTaskRunner() {
  // Use function-local static to avoid exit-time destructor warning
  static base::NoDestructor<scoped_refptr<base::SequencedTaskRunner>>
      task_runner(base::ThreadPool::CreateSequencedTaskRunner(
          {base::TaskPriority::USER_BLOCKING,
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN,
           base::MayBlock()}));
  return *task_runner;
}

bool WindowsToastNotification::Initialize() {
  HRESULT hr = Windows::Foundation::Initialize(RO_INIT_SINGLETHREADED);
  if (FAILED(hr))
    Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);

  ScopedHString toast_manager_str(
      RuntimeClass_Windows_UI_Notifications_ToastNotificationManager);
  if (!toast_manager_str.success())
    return false;

  if (!toast_manager_) {
    toast_manager_ = new ComPtr<
        ABI::Windows::UI::Notifications::IToastNotificationManagerStatics>();
  }

  if (FAILED(Windows::Foundation::GetActivationFactory(
          toast_manager_str, toast_manager_->GetAddressOf())))
    return false;

  if (!toast_notifier_) {
    toast_notifier_ =
        new ComPtr<ABI::Windows::UI::Notifications::IToastNotifier>();
  }

  if (IsRunningInDesktopBridge()) {
    // Ironically, the Desktop Bridge / UWP environment
    // requires us to not give Windows an appUserModelId.
    return SUCCEEDED(
        (*toast_manager_)
            ->CreateToastNotifier(toast_notifier_->GetAddressOf()));
  } else {
    ScopedHString app_id;
    if (!GetAppUserModelID(&app_id))
      return false;

    return SUCCEEDED((*toast_manager_)
                         ->CreateToastNotifierWithId(
                             app_id, toast_notifier_->GetAddressOf()));
  }
}

WindowsToastNotification::WindowsToastNotification(
    NotificationDelegate* delegate,
    NotificationPresenter* presenter)
    : Notification(delegate, presenter) {}

WindowsToastNotification::~WindowsToastNotification() {
  // Remove the notification on exit.
  if (toast_notification_) {
    RemoveCallbacks(toast_notification_.Get());
  }
}

// This method posts a request onto the toast background thread, which
// creates the toast xml then posts notification creation to the UI thread. This
// avoids blocking the UI for expensive XML parsing and COM initialization or
// the COM server becoming unavailable. All needed fields are captured before
// posting the task.
// The method will eventually result in a display or failure signal being posted
// back to the UI thread, where further callbacks (clicked, dismissed, failed)
// are handled by ToastEventHandler.
void WindowsToastNotification::Show(const NotificationOptions& options) {
  DebugLog("WindowsToastNotification::Show called");
  DebugLog(base::StrCat(
      {"toast_xml empty: ", options.toast_xml.empty() ? "yes" : "no"}));
  if (!options.toast_xml.empty()) {
    DebugLog(base::StrCat({"toast_xml length: ",
                           base::NumberToString(options.toast_xml.length())}));
  }

  // Capture all needed data on UI thread before posting to background thread
  std::string notif_id = notification_id();
  NotificationPresenter* presenter_ptr = presenter();
  base::WeakPtr<Notification> weak_this = GetWeakPtr();
  scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner =
      content::GetUIThreadTaskRunner({});

  DebugLog("Posting task to background thread");
  auto task_runner = GetToastTaskRunner();
  DebugLog(base::StrCat({"Task runner valid: ", task_runner ? "yes" : "no"}));

  // Post Show operation to background thread to prevent blocking
  // This is the main entry point for the notification creation process
  bool posted = task_runner->PostTask(
      FROM_HERE,
      base::BindOnce(
          &WindowsToastNotification::CreateToastNotificationOnBackgroundThread,
          options, presenter_ptr, notif_id, weak_this, ui_task_runner));
  DebugLog(base::StrCat(
      {"Task posted to background thread: ", posted ? "yes" : "no"}));
}

// Creates the toast XML on the background thread. If the XML is invalid, posts
// a failure event back to the UI thread. Otherwise, continues to create the
// toast notification on the background thread.
void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
    const NotificationOptions& options,
    NotificationPresenter* presenter,
    const std::string& notification_id,
    base::WeakPtr<Notification> weak_notification,
    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) {
  DebugLog("CreateToastXmlOnBackgroundThread called");
  ComPtr<IXmlDocument> toast_xml;

  if (!CreateToastXmlDocument(options, presenter, notification_id,
                              weak_notification, ui_task_runner, &toast_xml)) {
    return;  // Error already posted to UI thread
  }

  // Continue to create the toast notification
  ComPtr<ABI::Windows::UI::Notifications::IToastNotification>
      toast_notification;
  if (!CreateToastNotification(toast_xml, options, notification_id,
                               weak_notification, ui_task_runner,
                               &toast_notification)) {
    return;  // Error already posted to UI thread
  }

  // Setup callbacks and show on UI thread (Show must be called on UI thread)
  scoped_refptr<base::SingleThreadTaskRunner> ui_runner =
      content::GetUIThreadTaskRunner({});
  ui_runner->PostTask(
      FROM_HERE,
      base::BindOnce(&WindowsToastNotification::SetupAndShowOnUIThread,
                     weak_notification, toast_notification, options.group_id));
}

// Creates the toast XML document on the background thread. Returns true on
// success, false on failure. On failure, posts error to UI thread. static
bool WindowsToastNotification::CreateToastXmlDocument(
    const NotificationOptions& options,
    NotificationPresenter* presenter,
    const std::string& notification_id,
    base::WeakPtr<Notification> weak_notification,
    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
    ComPtr<IXmlDocument>* toast_xml) {
  // The custom xml takes priority over the preset template.
  if (!options.toast_xml.empty()) {
    DebugLog("Toast XML (custom) id=" + notification_id + ": " +
             base::UTF16ToUTF8(options.toast_xml));
    HRESULT hr = XmlDocumentFromString(base::as_wcstr(options.toast_xml),
                                       toast_xml->GetAddressOf());
    DebugLog(base::StrCat({"XmlDocumentFromString returned HRESULT: ",
                           base::NumberToString(hr)}));
    if (FAILED(hr)) {
      std::string err =
          base::StrCat({"XML: Invalid XML, ERROR ", FailureResultToString(hr)});
      DebugLog(base::StrCat({"XML parsing failed, posting error: ", err}));
      PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
      return false;
    }
    DebugLog("XML parsing succeeded");
  } else {
    auto* presenter_win = static_cast<NotificationPresenterWin*>(presenter);
    std::wstring icon_path =
        presenter_win->SaveIconToFilesystem(options.icon, options.icon_url);
    std::u16string toast_xml_str =
        GetToastXml(notification_id, options.title, options.msg, icon_path,
                    options.timeout_type, options.silent, options.actions,
                    options.has_reply, options.reply_placeholder,
                    options.group_id, options.group_title);

    DebugLog("Toast XML (generated) id=" + notification_id + ": " +
             base::UTF16ToUTF8(toast_xml_str));

    HRESULT hr = XmlDocumentFromString(base::as_wcstr(toast_xml_str),
                                       toast_xml->GetAddressOf());
    if (FAILED(hr)) {
      std::string err =
          base::StrCat({"XML: Invalid XML, ERROR ", FailureResultToString(hr)});
      DebugLog(err);
      PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
      return false;
    }
  }
  return true;
}

// Creates the toast notification on the background thread. Returns true on
// success, false on failure. On failure, posts error to UI thread. On success,
// returns the created notification via out parameter.
bool WindowsToastNotification::CreateToastNotification(
    ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> toast_xml,
    const NotificationOptions& options,
    const std::string& notification_id,
    base::WeakPtr<Notification> weak_notification,
    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
    ComPtr<ABI::Windows::UI::Notifications::IToastNotification>*
        toast_notification) {
  ScopedHString toast_str(
      RuntimeClass_Windows_UI_Notifications_ToastNotification);
  if (!toast_str.success()) {
    PostNotificationFailedToUIThread(
        weak_notification, "Creating ScopedHString failed", ui_task_runner);
    return false;
  }

  ComPtr<winui::Notifications::IToastNotificationFactory> toast_factory;
  HRESULT hr =
      Windows::Foundation::GetActivationFactory(toast_str, &toast_factory);
  if (FAILED(hr)) {
    std::string err =
        base::StrCat({"WinAPI: GetActivationFactory failed, ERROR ",
                      FailureResultToString(hr)});
    DebugLog(err);
    PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
    return false;
  }

  hr = toast_factory->CreateToastNotification(
      toast_xml.Get(), toast_notification->GetAddressOf());
  if (FAILED(hr)) {
    std::string err =
        base::StrCat({"WinAPI: CreateToastNotification failed, ERROR ",
                      FailureResultToString(hr)});
    DebugLog(err);
    PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
    return false;
  }

  ComPtr<winui::Notifications::IToastNotification2> toast2;
  hr = (*toast_notification)->QueryInterface(IID_PPV_ARGS(&toast2));
  if (FAILED(hr)) {
    std::string err =
        base::StrCat({"WinAPI: Getting Notification interface failed, ERROR ",
                      FailureResultToString(hr)});
    DebugLog(err);
    PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
    return false;
  }

  // Use provided group_id if non-empty, otherwise fall back to default
  std::wstring group_str =
      options.group_id.empty() ? kGroup : base::UTF8ToWide(options.group_id);
  ScopedHString group(group_str);
  DebugLog(
      base::StrCat({"Setting group: ", base::WideToUTF8(group_str),
                    options.group_id.empty() ? " (default)" : " (custom)"}));
  hr = toast2->put_Group(group);
  if (FAILED(hr)) {
    std::string err = base::StrCat(
        {"WinAPI: Setting group failed, ERROR ", FailureResultToString(hr)});
    DebugLog(err);
    PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
    return false;
  }

  std::wstring tag_str = GetTag(notification_id);
  ScopedHString tag(tag_str);
  DebugLog(base::StrCat({"Setting tag: ", base::WideToUTF8(tag_str),
                         " (from id: ", notification_id, ")"}));
  hr = toast2->put_Tag(tag);
  if (FAILED(hr)) {
    std::string err = base::StrCat(
        {"WinAPI: Setting tag failed, ERROR ", FailureResultToString(hr)});
    DebugLog(err);
    PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
    return false;
  }

  ComPtr<winui::Notifications::IToastNotification4> toast4;
  hr = (*toast_notification)->QueryInterface(IID_PPV_ARGS(&toast4));
  if (SUCCEEDED(hr)) {
    winui::Notifications::ToastNotificationPriority priority =
        winui::Notifications::ToastNotificationPriority::
            ToastNotificationPriority_Default;
    if (options.urgency == u"critical") {
      priority = winui::Notifications::ToastNotificationPriority::
          ToastNotificationPriority_High;
    }

    hr = toast4->put_Priority(priority);
    if (FAILED(hr)) {
      std::string err = base::StrCat({"WinAPI: Setting priority failed, ERROR ",
                                      FailureResultToString(hr)});
      DebugLog(err);
      PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
      return false;
    }
  }

  return true;
}

// Sets up callbacks and shows the notification on the UI thread.
// This part has to be called on the UI thread. This WinRT API
// does not allow calls from background threads.
void WindowsToastNotification::SetupAndShowOnUIThread(
    base::WeakPtr<Notification> weak_notification,
    ComPtr<ABI::Windows::UI::Notifications::IToastNotification> notification,
    const std::string& group_id) {
  auto* notif = static_cast<WindowsToastNotification*>(weak_notification.get());
  if (!notif)
    return;

  // Setup callbacks and store notification on UI thread
  HRESULT hr = notif->SetupCallbacks(notification.Get());
  if (FAILED(hr)) {
    std::string err = base::StrCat(
        {"WinAPI: SetupCallbacks failed, ERROR ", FailureResultToString(hr)});
    DebugLog(err);
    notif->NotificationFailed(err);
    return;
  }

  notif->toast_notification_ = notification;
  notif->group_id_ = group_id;

  // Show notification on UI thread (must be called on UI thread)
  hr = (*toast_notifier_)->Show(notification.Get());
  if (FAILED(hr)) {
    std::string err = base::StrCat(
        {"WinAPI: Show failed, ERROR ", FailureResultToString(hr)});
    DebugLog(err);
    notif->NotificationFailed(err);
    return;
  }

  DebugLog("Notification created");
  if (notif->delegate())
    notif->delegate()->NotificationDisplayed();
}

// Posts a notification failure event back to the UI thread. If the UI thread's
// task runner is not provided, it fetches it. On the UI thread, calls
// NotificationFailed on the Notification instance (if it is still valid), which
// will invoke the delegate (if set) and clean up.
void WindowsToastNotification::PostNotificationFailedToUIThread(
    base::WeakPtr<Notification> weak_notification,
    const std::string& error,
    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) {
  DebugLog(base::StrCat(
      {"PostNotificationFailedToUIThread called with error: ", error}));
  if (!ui_task_runner) {
    ui_task_runner = content::GetUIThreadTaskRunner({});
  }
  ui_task_runner->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](base::WeakPtr<Notification> weak_notification,
             const std::string& error) {
            DebugLog(
                "PostNotificationFailedToUIThread lambda executing on UI "
                "thread");
            if (!weak_notification) {
              DebugLog(base::StrCat(
                  {"Notification failed but object destroyed: ", error}));
              return;
            }

            // Call NotificationFailed - it will check for delegate internally
            // and emit the event if delegate is set
            DebugLog(base::StrCat(
                {"Calling NotificationFailed with error: ", error}));
            auto* delegate = weak_notification->delegate();
            DebugLog(
                base::StrCat({"Delegate is set: ", delegate ? "yes" : "no"}));

            // Call NotificationFailed which will call delegate if set, then
            // cleanup
            weak_notification->NotificationFailed(error);
            DebugLog("NotificationFailed call completed");
          },
          weak_notification, error));
}

void WindowsToastNotification::Remove() {
  DebugLog("Removing notification from action center");

  ComPtr<winui::Notifications::IToastNotificationManagerStatics2>
      toast_manager2;
  if (FAILED(toast_manager_->As(&toast_manager2)))
    return;

  ComPtr<winui::Notifications::IToastNotificationHistory> notification_history;
  if (FAILED(toast_manager2->get_History(&notification_history)))
    return;

  ScopedHString app_id;
  if (!GetAppUserModelID(&app_id))
    return;

  // Use stored group_id if set, otherwise fall back to default
  std::wstring group_str =
      group_id_.empty() ? kGroup : base::UTF8ToWide(group_id_);
  ScopedHString group(group_str);
  std::wstring tag_str = GetTag(notification_id());
  ScopedHString tag(tag_str);
  DebugLog(base::StrCat({"Removing with group: ", base::WideToUTF8(group_str),
                         ", tag: ", base::WideToUTF8(tag_str)}));
  notification_history->RemoveGroupedTagWithId(tag, group, app_id);
}

void WindowsToastNotification::Dismiss() {
  DebugLog("Hiding notification");

  (*toast_notifier_)->Hide(toast_notification_.Get());
}

std::u16string WindowsToastNotification::GetToastXml(
    const std::string& notification_id,
    const std::u16string& title,
    const std::u16string& msg,
    const std::wstring& icon_path,
    const std::u16string& timeout_type,
    bool silent,
    const std::vector<NotificationAction>& actions,
    bool has_reply,
    const std::u16string& reply_placeholder,
    const std::string& group_id,
    const std::u16string& group_title) {
  XmlWriter xml_writer;
  xml_writer.StartWriting();

  // <toast ...>
  xml_writer.StartElement(kToast);

  const bool is_reminder = (timeout_type == u"never");
  if (is_reminder) {
    xml_writer.AddAttribute(kScenario, kReminder);
  }

  // Add header element if both groupId and groupTitle are present
  if (!group_id.empty() && !group_title.empty()) {
    xml_writer.StartElement("header");
    xml_writer.AddAttribute("id", group_id);
    xml_writer.AddAttribute("title", base::UTF16ToUTF8(group_title));
    xml_writer.AddAttribute("arguments", "");
    xml_writer.EndElement();  // </header>
  }

  // <visual>
  xml_writer.StartElement(kVisual);
  // <binding template="<template>">
  xml_writer.StartElement(kBinding);
  xml_writer.AddAttribute(kTemplate, kToastTemplate);

  std::u16string line1;
  std::u16string line2;
  if (title.empty() || msg.empty()) {
    line1 = title.empty() ? msg : title;
    if (line1.empty())
      line1 = u"[no message]";
    // <text>
    xml_writer.StartElement(kText);
    xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
    xml_writer.EndElement();  // </text>
  } else {
    line1 = title;
    line2 = msg;
    // <text>
    xml_writer.StartElement(kText);
    xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
    xml_writer.EndElement();  // </text>
    // <text>
    xml_writer.StartElement(kText);
    xml_writer.AppendElementContent(base::UTF16ToUTF8(line2));
    xml_writer.EndElement();  // </text>
  }

  if (!icon_path.empty()) {
    // <image>
    xml_writer.StartElement(kImage);
    xml_writer.AddAttribute(kID, "1");
    xml_writer.AddAttribute(kPlacement, kAppLogoOverride);
    xml_writer.AddAttribute(kHintCrop, kHintCropNone);
    xml_writer.AddAttribute(kSrc, base::WideToUTF8(icon_path));
    xml_writer.EndElement();  // </image>
  }

  xml_writer.EndElement();  // </binding>
  xml_writer.EndElement();  // </visual>

  if (is_reminder || has_reply || !actions.empty()) {
    // <actions>
    xml_writer.StartElement(kActions);
    if (is_reminder) {
      xml_writer.StartElement(kAction);
      xml_writer.AddAttribute(kActivationType, kActivationTypeSystem);
      xml_writer.AddAttribute(kArguments, kDismiss);
      xml_writer.AddAttribute(
          kContent, base::WideToUTF8(l10n_util::GetWideString(IDS_APP_CLOSE)));
      xml_writer.EndElement();  // </action>
    }

    if (has_reply) {
      // <input>
      xml_writer.StartElement(kInput);
      xml_writer.AddAttribute(kID, kReply);
      xml_writer.AddAttribute(kType, kText);
      if (!reply_placeholder.empty()) {
        xml_writer.AddAttribute(kPlaceholderContent,
                                base::UTF16ToUTF8(reply_placeholder));
      }
      xml_writer.EndElement();  // </input>
    }

    for (size_t i = 0; i < actions.size(); ++i) {
      const auto& act = actions[i];
      if (act.type == u"button" || act.type.empty()) {
        // <action>
        xml_writer.StartElement(kAction);
        xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
        std::string args =
            base::StrCat({"type=action&action=", base::NumberToString(i),
                          "&tag=", notification_id});
        xml_writer.AddAttribute(kArguments, args.c_str());
        xml_writer.AddAttribute(kContent, base::UTF16ToUTF8(act.text));
        xml_writer.EndElement();  // <action>
      } else if (act.type == u"selection") {
        std::string input_id =
            base::StrCat({kSelection, base::NumberToString(i)});
        xml_writer.StartElement(kInput);  // <input>
        xml_writer.AddAttribute(kID, input_id.c_str());
        xml_writer.AddAttribute(kType, kSelection);
        for (size_t opt_i = 0; opt_i < act.items.size(); ++opt_i) {
          xml_writer.StartElement(kSelection);  // <selection>
          xml_writer.AddAttribute(kID, base::NumberToString(opt_i).c_str());
          xml_writer.AddAttribute(kContent,
                                  base::UTF16ToUTF8(act.items[opt_i]));
          xml_writer.EndElement();  // </selection>
        }
        xml_writer.EndElement();  // </input>

        // The button that submits the selection.
        xml_writer.StartElement(kAction);
        xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
        std::string args =
            base::StrCat({"type=action&action=", base::NumberToString(i),
                          "&tag=", notification_id});
        xml_writer.AddAttribute(kArguments, args.c_str());
        xml_writer.AddAttribute(
            kContent,
            base::UTF16ToUTF8(act.text.empty() ? u"Select" : act.text));
        xml_writer.AddAttribute(kHintInputId, input_id.c_str());
        xml_writer.EndElement();  // </action>
      }
    }

    if (has_reply) {
      // <action>
      xml_writer.StartElement(kAction);
      xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
      std::string args = base::StrCat({"type=reply&tag=", notification_id});
      xml_writer.AddAttribute(kArguments, args.c_str());
      // TODO(codebytere): we should localize this.
      xml_writer.AddAttribute(kContent, "Reply");
      xml_writer.AddAttribute(kHintInputId, kReply);
      xml_writer.EndElement();  // <action>
    }
    xml_writer.EndElement();  // </actions>
  }

  // Silent audio if requested.
  if (silent) {
    xml_writer.StartElement(kAudio);
    xml_writer.AddAttribute(kSilent, kTrue);
    xml_writer.EndElement();  // </audio>
  }

  xml_writer.EndElement();  // </toast>

  xml_writer.StopWriting();
  std::string xml = xml_writer.GetWrittenString();
  if (base::StartsWith(xml, kXmlVersionHeader, base::CompareCase::SENSITIVE)) {
    xml.erase(0, sizeof(kXmlVersionHeader) - 1);
  }

  return base::UTF8ToUTF16(xml);
}

HRESULT WindowsToastNotification::XmlDocumentFromString(
    const wchar_t* xmlString,
    IXmlDocument** doc) {
  ComPtr<IXmlDocument> xmlDoc;
  RETURN_IF_FAILED(Windows::Foundation::ActivateInstance(
      HStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument).Get(),
      &xmlDoc));

  ComPtr<IXmlDocumentIO> docIO;
  RETURN_IF_FAILED(xmlDoc.As(&docIO));

  RETURN_IF_FAILED(docIO->LoadXml(HStringReference(xmlString).Get()));

  return xmlDoc.CopyTo(doc);
}

HRESULT WindowsToastNotification::SetupCallbacks(
    winui::Notifications::IToastNotification* toast) {
  event_handler_ = Make<ToastEventHandler>(this);
  RETURN_IF_FAILED(
      toast->add_Activated(event_handler_.Get(), &activated_token_));
  RETURN_IF_FAILED(
      toast->add_Dismissed(event_handler_.Get(), &dismissed_token_));
  RETURN_IF_FAILED(toast->add_Failed(event_handler_.Get(), &failed_token_));
  return S_OK;
}

bool WindowsToastNotification::RemoveCallbacks(
    winui::Notifications::IToastNotification* toast) {
  if (FAILED(toast->remove_Activated(activated_token_)))
    return false;

  if (FAILED(toast->remove_Dismissed(dismissed_token_)))
    return false;

  return SUCCEEDED(toast->remove_Failed(failed_token_));
}

/*
/ Toast Event Handler
*/
ToastEventHandler::ToastEventHandler(Notification* notification)
    : notification_(notification->GetWeakPtr()) {}

ToastEventHandler::~ToastEventHandler() = default;

namespace {

// Extracts string user-input values from an IToastActivatedEventArgs2.
// Windows only fires the WinRT Activated event (not the COM activator) for
// `activationType="foreground"` actions while the app is already running, so
// reply text and selection values must be pulled from UserInput here rather
// than from the COM callback's NOTIFICATION_USER_INPUT_DATA.
std::vector<ActivationUserInput> ExtractUserInputs(IInspectable* args) {
  std::vector<ActivationUserInput> inputs;
  if (!args)
    return inputs;

  ComPtr<winui::Notifications::IToastActivatedEventArgs2> args2;
  if (FAILED(args->QueryInterface(IID_PPV_ARGS(&args2))) || !args2)
    return inputs;

  ComPtr<IPropertySet> user_input;
  if (FAILED(args2->get_UserInput(&user_input)) || !user_input)
    return inputs;

  ComPtr<IMap<HSTRING, IInspectable*>> map;
  if (FAILED(user_input.As(&map)) || !map)
    return inputs;

  ComPtr<IIterable<IKeyValuePair<HSTRING, IInspectable*>*>> iterable;
  if (FAILED(map.As(&iterable)) || !iterable)
    return inputs;

  ComPtr<IIterator<IKeyValuePair<HSTRING, IInspectable*>*>> iter;
  if (FAILED(iterable->First(&iter)) || !iter)
    return inputs;

  boolean has_current = false;
  if (FAILED(iter->get_HasCurrent(&has_current)))
    return inputs;

  while (has_current) {
    ComPtr<IKeyValuePair<HSTRING, IInspectable*>> kvp;
    if (FAILED(iter->get_Current(&kvp)) || !kvp)
      break;
    ScopedHString key_hs;
    ComPtr<IInspectable> value;
    if (SUCCEEDED(kvp->get_Key(key_hs.Receive())) &&
        SUCCEEDED(kvp->get_Value(&value)) && key_hs.success() && value) {
      ComPtr<IPropertyValue> prop;
      ScopedHString value_hs;
      if (SUCCEEDED(value.As(&prop)) && prop &&
          SUCCEEDED(prop->GetString(value_hs.Receive())) &&
          value_hs.success()) {
        UINT32 key_len = 0;
        UINT32 val_len = 0;
        const wchar_t* key_raw = WindowsGetStringRawBuffer(key_hs, &key_len);
        const wchar_t* val_raw = WindowsGetStringRawBuffer(value_hs, &val_len);
        ActivationUserInput ui;
        if (key_raw && key_len)
          ui.key.assign(key_raw, key_len);
        if (val_raw && val_len)
          ui.value.assign(val_raw, val_len);
        inputs.push_back(std::move(ui));
      }
    }
    if (FAILED(iter->MoveNext(&has_current)))
      break;
  }
  return inputs;
}

}  // namespace

IFACEMETHODIMP ToastEventHandler::Invoke(
    winui::Notifications::IToastNotification* sender,
    IInspectable* args) {
  std::wstring arguments_w;

  if (args) {
    ComPtr<winui::Notifications::IToastActivatedEventArgs> activated_args;
    if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&activated_args)))) {
      ScopedHString args_hs;
      if (SUCCEEDED(activated_args->get_Arguments(args_hs.Receive())) &&
          args_hs.success()) {
        UINT32 len = 0;
        const wchar_t* raw = WindowsGetStringRawBuffer(args_hs, &len);
        if (raw && len)
          arguments_w.assign(raw, len);
      }
    }
  }

  // For structured action/reply args, dispatch through the same handler the
  // COM activator uses. Previously this path early-returned, assuming the COM
  // INotificationActivationCallback would fire — but for non-MSIX apps with
  // activationType="foreground" (and for MSIX apps while already running)
  // Windows only raises the in-process WinRT Activated event, so those
  // action/reply events were being silently dropped. See electron/electron
  // issue #51147.
  const bool structured =
      arguments_w.find(L"type=action") != std::wstring::npos ||
      arguments_w.find(L"type=reply") != std::wstring::npos;
  if (structured) {
    std::vector<ActivationUserInput> inputs = ExtractUserInputs(args);
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&HandleToastActivation, arguments_w, std::move(inputs)));
    DebugLog("Notification activated (structured)");
    return S_OK;
  }

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&Notification::NotificationClicked, notification_));
  DebugLog("Notification clicked");

  return S_OK;
}

IFACEMETHODIMP ToastEventHandler::Invoke(
    winui::Notifications::IToastNotification* sender,
    winui::Notifications::IToastDismissedEventArgs* e) {
  winui::Notifications::ToastDismissalReason reason;
  std::string reason_string;
  if (SUCCEEDED(e->get_Reason(&reason))) {
    switch (reason) {
      case winui::Notifications::ToastDismissalReason_UserCanceled:
        reason_string = "userCanceled";
        break;
      case winui::Notifications::ToastDismissalReason_ApplicationHidden:
        reason_string = "applicationHidden";
        break;
      case winui::Notifications::ToastDismissalReason_TimedOut:
        reason_string = "timedOut";
        break;
    }
  }
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&Notification::NotificationDismissed,
                                notification_, false, reason_string));
  DebugLog("Notification dismissed");
  return S_OK;
}

IFACEMETHODIMP ToastEventHandler::Invoke(
    winui::Notifications::IToastNotification* sender,
    winui::Notifications::IToastFailedEventArgs* e) {
  HRESULT error;
  e->get_ErrorCode(&error);
  std::string errorMessage = FailureResultToString(error);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&Notification::NotificationFailed,
                                notification_, errorMessage));
  DebugLog(errorMessage);

  return S_OK;
}

}  // namespace electron
