#include "webview/webview.h"

#include <wrl.h>
#include <shellapi.h>
#include <winrt/Windows.Data.Json.h>

#include <algorithm>
#include <cmath>
#include <cwchar>
#include <limits>
#include <map>
#include <memory>
#include <unordered_map>
#include <utility>

#include <winrt/Windows.Foundation.h>

#include "util/composition.desktop.interop.h"
#include "util/logging.h"
#include "util/string_converter.h"
#include "webview/webview_host.h"

using namespace Microsoft::WRL;

namespace coreframe_flutter_windows {
namespace {

constexpr UINT kTrustedDecisionTimeoutMs = 5000;
constexpr double kMaxSafeJavaScriptInteger = 9007199254740991.0;
constexpr char kReleaseWebInteractionScript[] =
    "(()=>{const element=document.activeElement;const end=element?.selectionEnd;"
    "if(typeof end==='number'&&typeof element?.setSelectionRange==='function')"
    "element.setSelectionRange(end,end);globalThis.getSelection?.()?.removeAllRanges();"
    "if(element instanceof HTMLElement)element.blur();})()";

std::unordered_map<UINT_PTR, std::function<void()>> &DecisionTimers() {
  static std::unordered_map<UINT_PTR, std::function<void()>> timers;
  return timers;
}

void CALLBACK DecisionTimerCallback(HWND, UINT, UINT_PTR timer_id, DWORD) {
  KillTimer(nullptr, timer_id);
  auto &timers = DecisionTimers();
  const auto found = timers.find(timer_id);
  if (found == timers.end()) return;
  auto callback = std::move(found->second);
  timers.erase(found);
  callback();
}

UINT_PTR ScheduleDecisionTimeout(std::function<void()> callback) {
  const UINT_PTR timer_id =
      SetTimer(nullptr, 0, kTrustedDecisionTimeoutMs, DecisionTimerCallback);
  if (timer_id != 0) DecisionTimers()[timer_id] = std::move(callback);
  return timer_id;
}

std::unordered_map<UINT_PTR, std::function<void()>> &PresentationTimers() {
  static std::unordered_map<UINT_PTR, std::function<void()>> timers;
  return timers;
}

void CALLBACK PresentationTimerCallback(HWND, UINT, UINT_PTR timer_id, DWORD) {
  KillTimer(nullptr, timer_id);
  auto &timers = PresentationTimers();
  const auto found = timers.find(timer_id);
  if (found == timers.end()) return;
  auto callback = std::move(found->second);
  timers.erase(found);
  callback();
}

UINT_PTR SchedulePresentationTimer(std::function<void()> callback,
                                   UINT milliseconds) {
  const UINT_PTR timer_id =
      SetTimer(nullptr, 0, milliseconds, PresentationTimerCallback);
  if (timer_id != 0) PresentationTimers()[timer_id] = std::move(callback);
  return timer_id;
}

void CancelPresentationTimer(UINT_PTR timer_id) {
  if (timer_id == 0) return;
  KillTimer(nullptr, timer_id);
  PresentationTimers().erase(timer_id);
}

void CancelDecisionTimeout(UINT_PTR timer_id) {
  if (timer_id == 0) return;
  KillTimer(nullptr, timer_id);
  DecisionTimers().erase(timer_id);
}

inline void ConvertColor(COREWEBVIEW2_COLOR &webview_color, int32_t color) {
  webview_color.B = color & 0xFF;
  webview_color.G = (color >> 8) & 0xFF;
  webview_color.R = (color >> 16) & 0xFF;
  webview_color.A = (color >> 24) & 0xFF;
}

std::map<std::string, std::string>
ReadHttpHeaders(ICoreWebView2HttpHeadersCollectionIterator *iterator) {
  std::map<std::string, std::string> headers;
  if (iterator == nullptr) {
    return headers;
  }

  BOOL has_current = FALSE;
  if (FAILED(iterator->get_HasCurrentHeader(&has_current))) {
    return headers;
  }

  while (has_current) {
    wil::unique_cotaskmem_string name;
    wil::unique_cotaskmem_string value;
    if (SUCCEEDED(iterator->GetCurrentHeader(&name, &value)) &&
        name != nullptr && value != nullptr) {
      headers[util::Utf8FromUtf16(name.get())] =
          util::Utf8FromUtf16(value.get());
    }

    if (FAILED(iterator->MoveNext(&has_current))) {
      break;
    }
  }

  return headers;
}

std::map<std::string, std::string>
ReadHttpHeaders(ICoreWebView2HttpRequestHeaders *headers) {
  wil::com_ptr<ICoreWebView2HttpHeadersCollectionIterator> iterator;
  if (headers == nullptr || FAILED(headers->GetIterator(iterator.put()))) {
    return {};
  }
  return ReadHttpHeaders(iterator.get());
}

std::map<std::string, std::string>
ReadHttpHeaders(ICoreWebView2HttpResponseHeaders *headers) {
  wil::com_ptr<ICoreWebView2HttpHeadersCollectionIterator> iterator;
  if (headers == nullptr || FAILED(headers->GetIterator(iterator.put()))) {
    return {};
  }
  return ReadHttpHeaders(iterator.get());
}

double GetFlutterScrollOffsetMultiplier() {
  constexpr UINT kDefaultLinesPerScroll = 3;
  UINT lines_per_scroll = kDefaultLinesPerScroll;
  SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &lines_per_scroll, 0);
  if (lines_per_scroll == 0 || lines_per_scroll == WHEEL_PAGESCROLL) {
    lines_per_scroll = kDefaultLinesPerScroll;
  }
  return static_cast<double>(lines_per_scroll) * 100.0 / 3.0;
}

inline WebviewPermissionKind
CW2PermissionKindToPermissionKind(COREWEBVIEW2_PERMISSION_KIND kind) {
  using k = COREWEBVIEW2_PERMISSION_KIND;
  switch (kind) {
  case k::COREWEBVIEW2_PERMISSION_KIND_MICROPHONE:
    return WebviewPermissionKind::Microphone;
  case k::COREWEBVIEW2_PERMISSION_KIND_CAMERA:
    return WebviewPermissionKind::Camera;
  case k::COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION:
    return WebviewPermissionKind::GeoLocation;
  case k::COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS:
    return WebviewPermissionKind::Notifications;
  case k::COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS:
    return WebviewPermissionKind::OtherSensors;
  case k::COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ:
    return WebviewPermissionKind::ClipboardRead;
  default:
    return WebviewPermissionKind::Unknown;
  }
}

inline COREWEBVIEW2_PERMISSION_STATE
WebViewPermissionStateToCW2PermissionState(WebviewPermissionState state) {
  using s = COREWEBVIEW2_PERMISSION_STATE;
  switch (state) {
  case WebviewPermissionState::Allow:
    return s::COREWEBVIEW2_PERMISSION_STATE_ALLOW;
  case WebviewPermissionState::Deny:
    return s::COREWEBVIEW2_PERMISSION_STATE_DENY;
  default:
    return s::COREWEBVIEW2_PERMISSION_STATE_DEFAULT;
  }
}

inline WebviewJavaScriptDialogKind CW2ScriptDialogKindToJavaScriptDialogKind(
    COREWEBVIEW2_SCRIPT_DIALOG_KIND kind) {
  using k = COREWEBVIEW2_SCRIPT_DIALOG_KIND;
  switch (kind) {
  case k::COREWEBVIEW2_SCRIPT_DIALOG_KIND_ALERT:
    return WebviewJavaScriptDialogKind::Alert;
  case k::COREWEBVIEW2_SCRIPT_DIALOG_KIND_CONFIRM:
    return WebviewJavaScriptDialogKind::Confirm;
  case k::COREWEBVIEW2_SCRIPT_DIALOG_KIND_PROMPT:
    return WebviewJavaScriptDialogKind::Prompt;
  case k::COREWEBVIEW2_SCRIPT_DIALOG_KIND_BEFOREUNLOAD:
    return WebviewJavaScriptDialogKind::BeforeUnload;
  default:
    return WebviewJavaScriptDialogKind::Alert;
  }
}

inline bool ReadCookieString(ICoreWebView2Cookie *cookie,
                             HRESULT (ICoreWebView2Cookie::*getter)(LPWSTR *),
                             std::string &value) {
  wil::unique_cotaskmem_string raw_value;
  if (FAILED((cookie->*getter)(&raw_value))) {
    return false;
  }
  value = util::Utf8FromUtf16(raw_value.get());
  return true;
}

bool ApplyCookieProperties(ICoreWebView2Cookie *native_cookie,
                           const WebviewCookie &cookie) {
  if (cookie.expires.has_value() &&
      FAILED(native_cookie->put_Expires(cookie.expires.value()))) {
    return false;
  }
  if (cookie.is_http_only.has_value() &&
      FAILED(native_cookie->put_IsHttpOnly(cookie.is_http_only.value()))) {
    return false;
  }
  if (cookie.is_secure.has_value() &&
      FAILED(native_cookie->put_IsSecure(cookie.is_secure.value()))) {
    return false;
  }
  if (cookie.same_site.has_value() &&
      FAILED(native_cookie->put_SameSite(
          static_cast<COREWEBVIEW2_COOKIE_SAME_SITE_KIND>(
              cookie.same_site.value())))) {
    return false;
  }
  return true;
}

bool CreateNativeCookie(ICoreWebView2CookieManager *cookie_manager,
                        const WebviewCookie &cookie,
                        wil::com_ptr<ICoreWebView2Cookie> &native_cookie) {
  const std::wstring name = util::Utf16FromUtf8(cookie.name);
  const std::wstring value = util::Utf16FromUtf8(cookie.value);
  const std::wstring domain = util::Utf16FromUtf8(cookie.domain);
  const std::wstring path = util::Utf16FromUtf8(cookie.path);

  if (FAILED(cookie_manager->CreateCookie(name.c_str(), value.c_str(),
                                          domain.c_str(), path.c_str(),
                                          native_cookie.put()))) {
    return false;
  }
  return ApplyCookieProperties(native_cookie.get(), cookie);
}

std::string CanonicalizeNavigationUrl(const std::string &url) {
  try {
    const winrt::Windows::Foundation::Uri uri(winrt::to_hstring(url));
    return winrt::to_string(uri.AbsoluteCanonicalUri());
  } catch (...) {
    return url;
  }
}

bool IsExternalHttpUrl(const std::string &url) {
  try {
    const winrt::Windows::Foundation::Uri uri(winrt::to_hstring(url));
    const std::string scheme = winrt::to_string(uri.SchemeName());
    return (scheme == "http" || scheme == "https") &&
           !winrt::to_string(uri.Host()).empty() &&
           winrt::to_string(uri.UserName()).empty();
  } catch (...) {
    return false;
  }
}

std::string JavaScriptStringLiteral(const std::string &value) {
  std::string result = "\"";
  for (const unsigned char byte : value) {
    switch (byte) {
    case '\\': result += "\\\\"; break;
    case '"': result += "\\\""; break;
    case '\n': result += "\\n"; break;
    case '\r': result += "\\r"; break;
    case '\t': result += "\\t"; break;
    default:
      if (byte >= 0x20) result.push_back(static_cast<char>(byte));
      break;
    }
  }
  result.push_back('"');
  return result;
}

std::optional<PresentationIdentity>
PresentationIdentityFromScriptResult(const std::string &json) {
  try {
    const auto object = winrt::Windows::Data::Json::JsonObject::Parse(
        winrt::to_hstring(json));
    const std::string entry_key =
        winrt::to_string(object.GetNamedString(L"entryKey", L""));
    const std::string root_web_runtime_id = winrt::to_string(
        object.GetNamedString(L"rootWebRuntimeId", L""));
    const std::string application_scope_id = winrt::to_string(
        object.GetNamedString(L"applicationScopeId", L""));
    const double revision = object.GetNamedNumber(L"presentationRevision", 0);
    if (entry_key.empty() || root_web_runtime_id.empty() ||
        application_scope_id.empty() || !std::isfinite(revision) ||
        revision < 1 || std::floor(revision) != revision ||
        revision > kMaxSafeJavaScriptInteger) {
      return std::nullopt;
    }
    return PresentationIdentity{
        entry_key,
        static_cast<int64_t>(revision),
        root_web_runtime_id,
        application_scope_id,
    };
  } catch (...) {
    return std::nullopt;
  }
}

bool SamePresentationIdentity(const PresentationIdentity &left,
                              const PresentationIdentity &right) {
  return left.entry_key == right.entry_key &&
         left.presentation_revision == right.presentation_revision &&
         left.root_web_runtime_id == right.root_web_runtime_id &&
         left.application_scope_id == right.application_scope_id;
}

wil::com_ptr<IStream> CreateCaptureStream() {
  HGLOBAL global = GlobalAlloc(GMEM_MOVEABLE, 1);
  if (global == nullptr) return nullptr;
  IStream *raw_stream = nullptr;
  if (FAILED(CreateStreamOnHGlobal(global, TRUE, &raw_stream))) {
    GlobalFree(global);
    return nullptr;
  }
  wil::com_ptr<IStream> stream;
  stream.attach(raw_stream);
  return stream;
}

std::vector<uint8_t> ReadCaptureStream(IStream *stream) {
  if (stream == nullptr) return {};
  LARGE_INTEGER origin{};
  if (FAILED(stream->Seek(origin, STREAM_SEEK_SET, nullptr))) return {};
  STATSTG status{};
  if (FAILED(stream->Stat(&status, STATFLAG_NONAME)) ||
      status.cbSize.QuadPart == 0 || status.cbSize.QuadPart > 64 * 1024 * 1024) {
    return {};
  }
  std::vector<uint8_t> bytes(static_cast<size_t>(status.cbSize.QuadPart));
  ULONG read = 0;
  if (FAILED(stream->Read(bytes.data(), static_cast<ULONG>(bytes.size()),
                          &read)) ||
      read != static_cast<ULONG>(bytes.size())) {
    return {};
  }
  return bytes;
}

bool HasTrustedBridgeToken(const std::string &message,
                            const std::string &expected_token) {
  try {
    const auto object = winrt::Windows::Data::Json::JsonObject::Parse(
        winrt::to_hstring(message));
    return winrt::to_string(
               object.GetNamedString(L"coreframeToken", L"")) ==
           expected_token;
  } catch (...) {
    return false;
  }
}

} // namespace

Webview::Webview(
    wil::com_ptr<ICoreWebView2CompositionController> composition_controller,
    WebviewHost *host, HWND hwnd, bool owns_window, bool offscreen_only)
    : composition_controller_(std::move(composition_controller)), host_(host),
      hwnd_(hwnd), owns_window_(owns_window) {
  lifetime_state_->owner = this;
  webview_controller_ =
      composition_controller_.try_query<ICoreWebView2Controller3>();

  if (!webview_controller_ ||
      FAILED(webview_controller_->get_CoreWebView2(webview_.put()))) {
    return;
  }

  if (FAILED(webview_controller_->put_BoundsMode(
          COREWEBVIEW2_BOUNDS_MODE_USE_RAW_PIXELS)) ||
      FAILED(webview_controller_->put_ShouldDetectMonitorScaleChanges(FALSE)) ||
      FAILED(webview_controller_->put_RasterizationScale(1.0))) {
    return;
  }

  wil::com_ptr<ICoreWebView2Settings> settings;
  if (SUCCEEDED(webview_->get_Settings(settings.put()))) {
    settings_ = settings;
    settings2_ = settings.try_query<ICoreWebView2Settings2>();
    if (settings2_) {
      wil::unique_cotaskmem_string default_user_agent;
      if (SUCCEEDED(settings2_->get_UserAgent(&default_user_agent)) &&
          default_user_agent != nullptr) {
        default_user_agent_ = util::Utf8FromUtf16(default_user_agent.get());
      }
    }

    settings->put_IsStatusBarEnabled(FALSE);
    settings->put_AreDefaultContextMenusEnabled(FALSE);
  }

  EnableSecurityUpdates();
  RegisterEventHandlers();

  is_valid_ = CreateSurface(host->compositor(), hwnd, offscreen_only);
}

Webview::~Webview() {
  CancelTrustedNavigations();
  InvalidatePendingNavigationRequests();
  trusted_dispatch_in_progress_ = false;
  presentation_capture_in_progress_ = false;
  lifetime_state_->owner = nullptr;
  navigation_requested_callback_ = nullptr;
  download_event_callback_ = nullptr;
  if (owns_window_) {
    DestroyWindow(hwnd_);
  }
}

bool Webview::CreateSurface(
    winrt::com_ptr<ABI::Windows::UI::Composition::ICompositor> compositor,
    HWND hwnd, bool offscreen_only) {
  if (!compositor || !composition_controller_ || !webview_controller_) {
    return false;
  }

  winrt::com_ptr<ABI::Windows::UI::Composition::IContainerVisual> root;
  if (FAILED(compositor->CreateContainerVisual(root.put())) || !root) {
    return false;
  }

  surface_ = root.try_as<ABI::Windows::UI::Composition::IVisual>();
  if (!surface_) {
    return false;
  }

  // initial size. doesn't matter as we resize the surface anyway.
  if (FAILED(surface_->put_Size({1280, 720})) ||
      FAILED(surface_->put_IsVisible(true))) {
    return false;
  }

  // Create on-screen window for debugging purposes
  if (!offscreen_only) {
    window_target_ = util::TryCreateDesktopWindowTarget(compositor, hwnd);
    auto composition_target =
        window_target_
            .try_as<ABI::Windows::UI::Composition::ICompositionTarget>();
    if (!composition_target ||
        FAILED(composition_target->put_Root(surface_.get()))) {
      return false;
    }
  }

  winrt::com_ptr<ABI::Windows::UI::Composition::IVisual> webview_visual;
  if (FAILED(compositor->CreateContainerVisual(
          reinterpret_cast<ABI::Windows::UI::Composition::IContainerVisual **>(
              webview_visual.put()))) ||
      !webview_visual) {
    return false;
  }

  auto webview_visual2 =
      webview_visual.try_as<ABI::Windows::UI::Composition::IVisual2>();
  if (!webview_visual2 ||
      FAILED(webview_visual2->put_RelativeSizeAdjustment({1.0f, 1.0f}))) {
    return false;
  }

  winrt::com_ptr<ABI::Windows::UI::Composition::IVisualCollection> children;
  if (FAILED(root->get_Children(children.put())) || !children ||
      FAILED(children->InsertAtTop(webview_visual.get())) ||
      FAILED(composition_controller_->put_RootVisualTarget(
          webview_visual2.get())) ||
      FAILED(webview_controller_->put_IsVisible(true))) {
    return false;
  }

  return true;
}

void Webview::EnableSecurityUpdates() {
  if (SUCCEEDED(webview_->CallDevToolsProtocolMethod(L"Security.enable", L"{}",
                                                     nullptr)) &&
      SUCCEEDED(webview_->GetDevToolsProtocolEventReceiver(
          L"Security.securityStateChanged",
          &devtools_protocol_event_receiver_))) {
    devtools_protocol_event_receiver_->add_DevToolsProtocolEventReceived(
        Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
            [this](ICoreWebView2 *sender,
                   ICoreWebView2DevToolsProtocolEventReceivedEventArgs *args)
                -> HRESULT {
              if (devtools_protocol_event_callback_) {
                wil::unique_cotaskmem_string json_args;
                if (args->get_ParameterObjectAsJson(&json_args) == S_OK) {
                  std::string json = util::Utf8FromUtf16(json_args.get());
                  devtools_protocol_event_callback_(json.c_str());
                }
              }

              return S_OK;
            })
            .Get(),
        &event_registrations_.devtools_protocol_event_token_);
  }
}

void Webview::RegisterEventHandlers() {
  if (!webview_) {
    return;
  }

  webview_->add_NavigationStarting(
      Callback<ICoreWebView2NavigationStartingEventHandler>(
          [this](ICoreWebView2 *sender,
                 ICoreWebView2NavigationStartingEventArgs *args) -> HRESULT {
            if (!args) {
              return S_OK;
            }

            wil::unique_cotaskmem_string wuri;
            if (FAILED(args->get_Uri(&wuri)) || wuri == nullptr) {
              return S_OK;
            }
            const std::string url = util::Utf8FromUtf16(wuri.get());

            if (trusted_configured_) {
              if (trusted_dispatch_in_progress_ && !IsTrustedUrl(url)) {
                args->put_Cancel(TRUE);
                if (trusted_security_event_callback_) {
                  trusted_security_event_callback_(
                      "navigationBlocked", url, "policy", trusted_epoch_);
                }
                return S_OK;
              }
              UINT64 navigation_id = 0;
              args->get_NavigationId(&navigation_id);
              if (ConsumeTrustedReplay(url, navigation_id)) return S_OK;
              const bool restore_active = trusted_active_;
              const bool restore_ready = trusted_ready_;
              const std::string restore_url = trusted_authorized_url_;
              CancelTrustedNavigations();
              ++trusted_epoch_;
              InvalidateTrustedDocument();
              trusted_navigation_id_ = 0;
              trusted_authorized_url_.clear();
              BOOL is_redirected = FALSE;
              BOOL is_user_initiated = FALSE;
              args->get_IsRedirected(&is_redirected);
              args->get_IsUserInitiated(&is_user_initiated);
              if (navigation_id != 0) {
                policy_cancelled_navigation_ids_.insert(navigation_id);
              }
              BeginTrustedNavigation(
                  args, url, "main",
                  ConsumeTrustedNavigationKind(url, is_redirected == TRUE),
                  is_user_initiated == TRUE, restore_active, restore_ready,
                  restore_url);
              return S_OK;
            }

            if (ConsumeApprovedNavigation(url) ||
                !navigation_request_callbacks_enabled_ ||
                !navigation_requested_callback_) {
              return S_OK;
            }

            if (FAILED(args->put_Cancel(TRUE))) {
              return S_OK;
            }

            UINT64 navigation_id = 0;
            if (SUCCEEDED(args->get_NavigationId(&navigation_id))) {
              policy_cancelled_navigation_ids_.insert(navigation_id);
            }

            BOOL is_user_initiated = FALSE;
            BOOL is_redirected = FALSE;
            args->get_IsUserInitiated(&is_user_initiated);
            args->get_IsRedirected(&is_redirected);

            const uint64_t request_id = ++latest_navigation_request_id_;
            const std::weak_ptr<LifetimeState> weak_state = lifetime_state_;
            navigation_requested_callback_(
                url, is_user_initiated == TRUE, is_redirected == TRUE,
                [weak_state, request_id, url](bool allow) {
                  if (!allow) {
                    return;
                  }
                  const std::shared_ptr<LifetimeState> state =
                      weak_state.lock();
                  if (state && state->owner) {
                    state->owner->ResumeNavigation(request_id, url);
                  }
                });
            return S_OK;
          })
          .Get(),
      &event_registrations_.navigation_starting_token_);

  webview_->add_FrameNavigationStarting(
      Callback<ICoreWebView2NavigationStartingEventHandler>(
          [this](ICoreWebView2 *,
                 ICoreWebView2NavigationStartingEventArgs *args) -> HRESULT {
            if (!trusted_configured_ || args == nullptr) return S_OK;
            wil::unique_cotaskmem_string uri;
            if (FAILED(args->get_Uri(&uri)) || uri == nullptr) {
              args->put_Cancel(TRUE);
              return S_OK;
            }
            const std::string url = util::Utf8FromUtf16(uri.get());
            // WebView2 cannot defer frame NavigationStarting events. This
            // synchronous prefilter is the same exact-origin rule as the Dart
            // child disposition; response CSP keeps an allowed child opaque.
            if (!trusted_active_ || !IsTrustedUrl(url)) {
              args->put_Cancel(TRUE);
              if (trusted_security_event_callback_) {
                trusted_security_event_callback_(
                    "navigationBlocked", url, "child-frame", trusted_epoch_);
              }
            }
            return S_OK;
          })
          .Get(),
      &event_registrations_.frame_navigation_starting_token_);

  // The legacy filter omits requests sourced by cross-origin iframes. Trusted
  // mode requires the source-aware filter so every document request is seen.
  auto webview22 = webview_.try_query<ICoreWebView2_22>();
  if (webview22 &&
      SUCCEEDED(webview22->AddWebResourceRequestedFilterWithRequestSourceKinds(
          L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT,
          COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_DOCUMENT))) {
    trusted_document_request_filter_ready_ =
        SUCCEEDED(webview_->add_WebResourceRequested(
            Callback<ICoreWebView2WebResourceRequestedEventHandler>(
                [this](ICoreWebView2 *,
                       ICoreWebView2WebResourceRequestedEventArgs *args)
                    -> HRESULT {
                  if (!trusted_configured_ || args == nullptr) return S_OK;

                  wil::com_ptr<ICoreWebView2WebResourceRequest> request;
                  wil::unique_cotaskmem_string uri;
                  if (SUCCEEDED(args->get_Request(request.put())) && request &&
                      SUCCEEDED(request->get_Uri(&uri)) && uri != nullptr) {
                    const std::string url = util::Utf8FromUtf16(uri.get());
                    if (IsTrustedUrl(url) || !IsExternalHttpUrl(url)) {
                      return S_OK;
                    }
                  }

                  // NavigationStarting cancellation can race with a GET. A
                  // local response prevents the request from reaching the
                  // untrusted server.
                  auto response = host_->CreateWebResourceResponse(
                      {}, 204, "No Content", "Cache-Control: no-store\r\n");
                  if (!response) return E_FAIL;
                  return args->put_Response(response.get());
                })
                .Get(),
            &event_registrations_.web_resource_requested_token_));
  }

  webview_->add_ContentLoading(
      Callback<ICoreWebView2ContentLoadingEventHandler>(
          [this](ICoreWebView2 *sender,
                 ICoreWebView2ContentLoadingEventArgs *args) -> HRESULT {
            if (trusted_configured_) {
              UINT64 navigation_id = 0;
              wil::unique_cotaskmem_string source;
              if (args != nullptr &&
                  SUCCEEDED(args->get_NavigationId(&navigation_id)) &&
                  navigation_id == trusted_navigation_id_ &&
                  SUCCEEDED(webview_->get_Source(&source)) &&
                  source != nullptr &&
                  util::Utf8FromUtf16(source.get()) ==
                      trusted_authorized_url_ &&
                  IsTrustedUrl(trusted_authorized_url_)) {
                trusted_active_ = true;
                if (trusted_security_event_callback_) {
                  trusted_security_event_callback_(
                      "documentActivated", trusted_authorized_url_, "",
                      trusted_epoch_);
                }
              }
            }
            if (loading_state_changed_callback_) {
              loading_state_changed_callback_(WebviewLoadingState::Loading);
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.content_loading_token_);

  webview_->add_NavigationCompleted(
      Callback<ICoreWebView2NavigationCompletedEventHandler>(
          [this](ICoreWebView2 *sender,
                 ICoreWebView2NavigationCompletedEventArgs *args) -> HRESULT {
            UINT64 navigation_id = 0;
            if (SUCCEEDED(args->get_NavigationId(&navigation_id)) &&
                policy_cancelled_navigation_ids_.erase(navigation_id) > 0) {
              return S_OK;
            }

            BOOL is_success;
            args->get_IsSuccess(&is_success);
            const bool current_trusted_navigation =
                trusted_configured_ && navigation_id == trusted_navigation_id_;
            if (current_trusted_navigation && !is_success) {
              InvalidateTrustedDocument();
              if (trusted_security_event_callback_) {
                trusted_security_event_callback_("loadError", "",
                                                   "Navigation failed.",
                                                   trusted_epoch_);
              }
            }
            if (current_trusted_navigation && is_success &&
                trusted_active_ &&
                trusted_security_event_callback_) {
              wil::unique_cotaskmem_string source;
              if (SUCCEEDED(webview_->get_Source(&source)) &&
                  source != nullptr) {
                const std::string url = util::Utf8FromUtf16(source.get());
                if (CanonicalizeNavigationUrl(url) ==
                    CanonicalizeNavigationUrl(trusted_authorized_url_)) {
                  trusted_security_event_callback_("documentFinished", url,
                                                     "", trusted_epoch_);
                }
              }
            }
            if (!is_success && on_load_error_callback_) {
              COREWEBVIEW2_WEB_ERROR_STATUS web_error_status;
              args->get_WebErrorStatus(&web_error_status);
              on_load_error_callback_(web_error_status);
            }

            if (loading_state_changed_callback_) {
              loading_state_changed_callback_(
                  WebviewLoadingState::NavigationCompleted);
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.navigation_completed_token_);

  auto webview2 = webview_.try_query<ICoreWebView2_2>();
  if (webview2) {
    webview2->add_WebResourceResponseReceived(
        Callback<ICoreWebView2WebResourceResponseReceivedEventHandler>(
            [this](ICoreWebView2 *sender,
                   ICoreWebView2WebResourceResponseReceivedEventArgs *args)
                -> HRESULT {
              if (!http_response_error_callback_) {
                return S_OK;
              }

              wil::com_ptr<ICoreWebView2WebResourceResponseView> response;
              if (FAILED(args->get_Response(response.put())) || !response) {
                return S_OK;
              }

              int status_code = 0;
              if (FAILED(response->get_StatusCode(&status_code)) ||
                  status_code < 400) {
                return S_OK;
              }

              wil::com_ptr<ICoreWebView2WebResourceRequest> request;
              if (FAILED(args->get_Request(request.put())) || !request) {
                return S_OK;
              }

              wil::unique_cotaskmem_string wuri;
              if (FAILED(request->get_Uri(&wuri))) {
                return S_OK;
              }

              std::string url;
              if (wuri != nullptr) {
                url = util::Utf8FromUtf16(wuri.get());
              }

              std::string method;
              wil::unique_cotaskmem_string wmethod;
              if (SUCCEEDED(request->get_Method(&wmethod)) &&
                  wmethod != nullptr) {
                method = util::Utf8FromUtf16(wmethod.get());
              }

              std::map<std::string, std::string> request_headers;
              wil::com_ptr<ICoreWebView2HttpRequestHeaders>
                  native_request_headers;
              if (SUCCEEDED(
                      request->get_Headers(native_request_headers.put()))) {
                request_headers = ReadHttpHeaders(native_request_headers.get());
              }

              std::map<std::string, std::string> response_headers;
              wil::com_ptr<ICoreWebView2HttpResponseHeaders>
                  native_response_headers;
              if (SUCCEEDED(
                      response->get_Headers(native_response_headers.put()))) {
                response_headers =
                    ReadHttpHeaders(native_response_headers.get());
              }

              std::optional<std::string> reason_phrase;
              wil::unique_cotaskmem_string wreason_phrase;
              if (SUCCEEDED(response->get_ReasonPhrase(&wreason_phrase)) &&
                  wreason_phrase != nullptr) {
                reason_phrase = util::Utf8FromUtf16(wreason_phrase.get());
              }

              http_response_error_callback_({url, method, request_headers,
                                             status_code, response_headers,
                                             reason_phrase});
              return S_OK;
            })
            .Get(),
        &event_registrations_.web_resource_response_received_token_);
  }

  webview_->add_HistoryChanged(
      Callback<ICoreWebView2HistoryChangedEventHandler>(
          [this](ICoreWebView2 *sender, IUnknown *args) -> HRESULT {
            if (history_changed_callback_) {
              BOOL can_go_back;
              BOOL can_go_forward;
              sender->get_CanGoBack(&can_go_back);
              sender->get_CanGoForward(&can_go_forward);
              history_changed_callback_({can_go_back, can_go_forward});
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.history_changed_token_);

  webview_->add_SourceChanged(
      Callback<ICoreWebView2SourceChangedEventHandler>(
          [this](ICoreWebView2 *sender, IUnknown *args) -> HRESULT {
            wil::unique_cotaskmem_string wurl;
            if (url_changed_callback_ &&
                SUCCEEDED(webview_->get_Source(&wurl)) && wurl != nullptr) {
              std::string url = util::Utf8FromUtf16(wurl.get());
              url_changed_callback_(url);
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.source_changed_token_);

  webview_->add_DocumentTitleChanged(
      Callback<ICoreWebView2DocumentTitleChangedEventHandler>(
          [this](ICoreWebView2 *sender, IUnknown *args) -> HRESULT {
            wil::unique_cotaskmem_string wtitle;
            if (document_title_changed_callback_ &&
                SUCCEEDED(webview_->get_DocumentTitle(&wtitle)) &&
                wtitle != nullptr) {
              std::string title = util::Utf8FromUtf16(wtitle.get());
              document_title_changed_callback_(title);
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.document_title_changed_token_);

  composition_controller_->add_CursorChanged(
      Callback<ICoreWebView2CursorChangedEventHandler>(
          [this](ICoreWebView2CompositionController *sender,
                 IUnknown *args) -> HRESULT {
            HCURSOR cursor;
            if (cursor_changed_callback_ &&
                sender->get_Cursor(&cursor) == S_OK) {
              cursor_changed_callback_(cursor);
            }
            return S_OK;
          })
          .Get(),
      &event_registrations_.cursor_changed_token_);

  webview_controller_->add_GotFocus(
      Callback<ICoreWebView2FocusChangedEventHandler>(
          [this](ICoreWebView2Controller *sender, IUnknown *args) -> HRESULT {
            if (focus_changed_callback_) {
              focus_changed_callback_(true);
            }
            return S_OK;
          })
          .Get(),
      &event_registrations_.got_focus_token_);

  webview_controller_->add_LostFocus(
      Callback<ICoreWebView2FocusChangedEventHandler>(
          [this](ICoreWebView2Controller *sender, IUnknown *args) -> HRESULT {
            if (focus_changed_callback_) {
              focus_changed_callback_(false);
            }
            return S_OK;
          })
          .Get(),
      &event_registrations_.lost_focus_token_);

  webview_->add_WebMessageReceived(
      Callback<ICoreWebView2WebMessageReceivedEventHandler>(
          [this](ICoreWebView2 *sender,
                 ICoreWebView2WebMessageReceivedEventArgs *args) -> HRESULT {
            wil::unique_cotaskmem_string wmessage;
            wil::unique_cotaskmem_string wsource;
            if (web_message_received_callback_ &&
                args->get_WebMessageAsJson(&wmessage) == S_OK &&
                args->get_Source(&wsource) == S_OK && wmessage != nullptr &&
                wsource != nullptr) {
              const std::string message = util::Utf8FromUtf16(wmessage.get());
              const std::string source = util::Utf8FromUtf16(wsource.get());
              if (!trusted_configured_ ||
                  (trusted_active_ && IsTrustedUrl(source) &&
                   HasTrustedBridgeToken(message,
                                           trusted_bridge_token_))) {
                web_message_received_callback_(source, message);
              }
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.web_message_received_token_);

  webview_->add_PermissionRequested(
      Callback<ICoreWebView2PermissionRequestedEventHandler>(
          [this](ICoreWebView2 *sender,
                 ICoreWebView2PermissionRequestedEventArgs *args) -> HRESULT {
            if (!permission_requested_callback_) {
              return S_OK;
            }

            wil::unique_cotaskmem_string wuri;
            COREWEBVIEW2_PERMISSION_KIND kind =
                COREWEBVIEW2_PERMISSION_KIND_UNKNOWN_PERMISSION;
            BOOL is_user_initiated = false;

            if (args->get_Uri(&wuri) == S_OK &&
                args->get_PermissionKind(&kind) == S_OK &&
                args->get_IsUserInitiated(&is_user_initiated) == S_OK) {
              wil::com_ptr<ICoreWebView2Deferral> deferral;
              if (FAILED(args->GetDeferral(deferral.put())) || !deferral) {
                return S_OK;
              }

              const std::string uri = util::Utf8FromUtf16(wuri.get());
              wil::com_ptr<ICoreWebView2PermissionRequestedEventArgs>
                  permission_args;
              args->AddRef();
              permission_args.attach(args);
              permission_requested_callback_(
                  uri, CW2PermissionKindToPermissionKind(kind),
                  is_user_initiated == TRUE,
                  [deferral = std::move(deferral),
                   permission_args = std::move(permission_args)](
                      WebviewPermissionState state) {
                    permission_args->put_State(
                        WebViewPermissionStateToCW2PermissionState(state));
                    deferral->Complete();
                  });
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.permission_requested_token_);

  auto webview10 = webview_.try_query<ICoreWebView2_10>();
  if (webview10) {
    webview10->add_BasicAuthenticationRequested(
        Callback<ICoreWebView2BasicAuthenticationRequestedEventHandler>(
            [this](ICoreWebView2 *sender,
                   ICoreWebView2BasicAuthenticationRequestedEventArgs *args)
                -> HRESULT {
              if (!http_auth_requested_callback_) {
                return S_OK;
              }

              wil::unique_cotaskmem_string wuri;
              wil::unique_cotaskmem_string wchallenge;
              wil::com_ptr<ICoreWebView2BasicAuthenticationResponse> response;
              if (FAILED(args->get_Uri(&wuri)) ||
                  FAILED(args->get_Challenge(&wchallenge)) ||
                  FAILED(args->get_Response(response.put())) || !response) {
                return S_OK;
              }

              wil::com_ptr<ICoreWebView2Deferral> deferral;
              if (FAILED(args->GetDeferral(deferral.put())) || !deferral) {
                return S_OK;
              }

              wil::com_ptr<ICoreWebView2BasicAuthenticationRequestedEventArgs>
                  auth_args;
              args->AddRef();
              auth_args.attach(args);

              WebviewHttpAuthRequest request{
                  util::Utf8FromUtf16(wuri.get()),
                  util::Utf8FromUtf16(wchallenge.get())};
              http_auth_requested_callback_(
                  request, [deferral = std::move(deferral),
                            auth_args = std::move(auth_args),
                            response = std::move(response)](
                               bool accepted, const std::string &user,
                               const std::string &password) mutable {
                    if (accepted) {
                      const std::wstring user_utf16 = util::Utf16FromUtf8(user);
                      const std::wstring password_utf16 =
                          util::Utf16FromUtf8(password);
                      response->put_UserName(user_utf16.c_str());
                      response->put_Password(password_utf16.c_str());
                      auth_args->put_Cancel(FALSE);
                    } else {
                      auth_args->put_Cancel(TRUE);
                    }
                    deferral->Complete();
                  });

              return S_OK;
            })
            .Get(),
        &event_registrations_.basic_authentication_requested_token_);
  }

  auto webview14 = webview_.try_query<ICoreWebView2_14>();
  if (webview14) {
    webview14->add_ServerCertificateErrorDetected(
        Callback<ICoreWebView2ServerCertificateErrorDetectedEventHandler>(
            [this](ICoreWebView2 *sender,
                   ICoreWebView2ServerCertificateErrorDetectedEventArgs *args)
                -> HRESULT {
              if (!ssl_auth_error_callback_) {
                args->put_Action(
                    COREWEBVIEW2_SERVER_CERTIFICATE_ERROR_ACTION_CANCEL);
                return S_OK;
              }

              COREWEBVIEW2_WEB_ERROR_STATUS status =
                  COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN;
              wil::unique_cotaskmem_string request_uri;
              if (FAILED(args->get_ErrorStatus(&status)) ||
                  FAILED(args->get_RequestUri(&request_uri))) {
                args->put_Action(
                    COREWEBVIEW2_SERVER_CERTIFICATE_ERROR_ACTION_CANCEL);
                return S_OK;
              }

              wil::com_ptr<ICoreWebView2Deferral> deferral;
              if (FAILED(args->GetDeferral(deferral.put())) || !deferral) {
                args->put_Action(
                    COREWEBVIEW2_SERVER_CERTIFICATE_ERROR_ACTION_CANCEL);
                return S_OK;
              }

              wil::com_ptr<ICoreWebView2ServerCertificateErrorDetectedEventArgs>
                  error_args;
              args->AddRef();
              error_args.attach(args);

              ssl_auth_error_callback_(
                  {util::Utf8FromUtf16(request_uri.get()), status},
                  [deferral = std::move(deferral),
                   error_args = std::move(error_args)](bool proceed) mutable {
                    error_args->put_Action(
                        proceed
                            ? COREWEBVIEW2_SERVER_CERTIFICATE_ERROR_ACTION_ALWAYS_ALLOW
                            : COREWEBVIEW2_SERVER_CERTIFICATE_ERROR_ACTION_CANCEL);
                    deferral->Complete();
                  });

              return S_OK;
            })
            .Get(),
        &event_registrations_.server_certificate_error_detected_token_);
  }

  webview_->add_ScriptDialogOpening(
      Callback<ICoreWebView2ScriptDialogOpeningEventHandler>(
          [this](ICoreWebView2 *sender,
                 ICoreWebView2ScriptDialogOpeningEventArgs *args) -> HRESULT {
            if (!java_script_dialog_requested_callback_) {
              return S_OK;
            }

            COREWEBVIEW2_SCRIPT_DIALOG_KIND raw_kind =
                COREWEBVIEW2_SCRIPT_DIALOG_KIND_ALERT;
            if (FAILED(args->get_Kind(&raw_kind))) {
              return S_OK;
            }

            const WebviewJavaScriptDialogKind kind =
                CW2ScriptDialogKindToJavaScriptDialogKind(raw_kind);
            bool enabled = false;
            switch (kind) {
            case WebviewJavaScriptDialogKind::Alert:
              enabled = java_script_alert_dialog_enabled_;
              break;
            case WebviewJavaScriptDialogKind::Confirm:
              enabled = java_script_confirm_dialog_enabled_;
              break;
            case WebviewJavaScriptDialogKind::Prompt:
              enabled = java_script_prompt_dialog_enabled_;
              break;
            case WebviewJavaScriptDialogKind::BeforeUnload:
              enabled = false;
              break;
            }
            if (!enabled) {
              return S_OK;
            }

            wil::unique_cotaskmem_string wuri;
            wil::unique_cotaskmem_string wmessage;
            if (FAILED(args->get_Uri(&wuri)) ||
                FAILED(args->get_Message(&wmessage))) {
              return S_OK;
            }

            WebviewJavaScriptDialogRequest request{
                kind, util::Utf8FromUtf16(wuri.get()),
                util::Utf8FromUtf16(wmessage.get()), std::nullopt};
            if (kind == WebviewJavaScriptDialogKind::Prompt) {
              wil::unique_cotaskmem_string wdefault_text;
              if (SUCCEEDED(args->get_DefaultText(&wdefault_text))) {
                request.default_text = util::Utf8FromUtf16(wdefault_text.get());
              }
            }

            wil::com_ptr<ICoreWebView2Deferral> deferral;
            if (FAILED(args->GetDeferral(deferral.put())) || !deferral) {
              return S_OK;
            }

            wil::com_ptr<ICoreWebView2ScriptDialogOpeningEventArgs> dialog_args;
            args->AddRef();
            dialog_args.attach(args);
            java_script_dialog_requested_callback_(
                request, [deferral = std::move(deferral),
                          dialog_args = std::move(dialog_args), kind](
                             bool accepted,
                             const std::optional<std::string> &text) mutable {
                  if (accepted) {
                    if (kind == WebviewJavaScriptDialogKind::Prompt && text) {
                      const std::wstring text_utf16 =
                          util::Utf16FromUtf8(*text);
                      dialog_args->put_ResultText(text_utf16.c_str());
                    }
                    dialog_args->Accept();
                  }
                  deferral->Complete();
                });

            return S_OK;
          })
          .Get(),
      &event_registrations_.script_dialog_opening_token_);

  webview_->add_NewWindowRequested(
      Callback<ICoreWebView2NewWindowRequestedEventHandler>(
          [this](ICoreWebView2 *sender,
                 ICoreWebView2NewWindowRequestedEventArgs *args) -> HRESULT {
            if (trusted_configured_) {
              args->put_Handled(TRUE);
              wil::unique_cotaskmem_string wuri;
              BOOL is_user_initiated = FALSE;
              args->get_IsUserInitiated(&is_user_initiated);
              std::string url;
              if (SUCCEEDED(args->get_Uri(&wuri)) && wuri != nullptr) {
                url = util::Utf8FromUtf16(wuri.get());
              }
              BeginTrustedPopup(args, url, is_user_initiated == TRUE);
              return S_OK;
            }
            switch (popup_window_policy_) {
            case WebviewPopupWindowPolicy::Deny:
              args->put_Handled(TRUE);
              break;
            case WebviewPopupWindowPolicy::ShowInSameWindow: {
              wil::unique_cotaskmem_string wuri;
              if (SUCCEEDED(args->get_Uri(&wuri)) && wuri != nullptr) {
                args->put_Handled(TRUE);
                webview_->Navigate(wuri.get());
              }
              break;
            }
            case WebviewPopupWindowPolicy::Allow:
              break;
            }

            return S_OK;
          })
          .Get(),
      &event_registrations_.new_windows_requested_token_);

  webview_->add_ContainsFullScreenElementChanged(
      Callback<ICoreWebView2ContainsFullScreenElementChangedEventHandler>(
          [this](ICoreWebView2 *sender, IUnknown *args) -> HRESULT {
            BOOL flag = FALSE;
            if (contains_fullscreen_element_changed_callback_ &&
                SUCCEEDED(sender->get_ContainsFullScreenElement(&flag))) {
              contains_fullscreen_element_changed_callback_(flag);
            }
            return S_OK;
          })
          .Get(),
      &event_registrations_.contains_fullscreen_element_changed_token_);

  auto webview24 = webview_.try_query<ICoreWebView2_4>();
  if (webview24) {
    webview24->add_DownloadStarting(
        Callback<ICoreWebView2DownloadStartingEventHandler>(
            [this](ICoreWebView2 *sender,
                   ICoreWebView2DownloadStartingEventArgs *args) -> HRESULT {
              args->put_Handled(TRUE);

              wil::com_ptr<ICoreWebView2DownloadOperation> download;
              args->get_DownloadOperation(&download);

              INT64 totalBytesToReceive = 0;
              download->get_TotalBytesToReceive(&totalBytesToReceive);

              wil::unique_cotaskmem_string uri;
              download->get_Uri(&uri);

              wil::unique_cotaskmem_string mimeType;
              download->get_MimeType(&mimeType);

              wil::unique_cotaskmem_string contentDisposition;
              download->get_ContentDisposition(&contentDisposition);

              wil::unique_cotaskmem_string resultFilePath;
              args->get_ResultFilePath(&resultFilePath);

              args->put_ResultFilePath(resultFilePath.get());
              UpdateDownloadProgress(download.get());

              if (download_event_callback_) {
                download_event_callback_(
                    {WebviewDownloadEventKind::DownloadStarted,
                     util::Utf8FromUtf16(uri.get()),
                     util::Utf8FromUtf16(resultFilePath.get()), 0,
                     totalBytesToReceive});
              }

              return S_OK;
            })
            .Get(),
        &event_registrations_.download_starting_token_);
  }
}

void Webview::SetSurfaceSize(size_t width, size_t height, float scale_factor) {
  if (!IsValid()) {
    return;
  }

  if (surface_ && width > 0 && height > 0) {
    scale_factor_ = scale_factor;
    auto scaled_width = width * scale_factor;
    auto scaled_height = height * scale_factor;

    RECT bounds;
    bounds.left = 0;
    bounds.top = 0;
    bounds.right = static_cast<LONG>(scaled_width);
    bounds.bottom = static_cast<LONG>(scaled_height);

    surface_->put_Size({scaled_width, scaled_height});
    webview_controller_->put_RasterizationScale(scale_factor);
    if (webview_controller_->put_Bounds(bounds) != S_OK) {
      util::LogWarning("Setting webview bounds failed.");
    }

    if (surface_size_changed_callback_) {
      surface_size_changed_callback_(width, height);
    }
  }
}

bool Webview::OpenDevTools() {
  if (!IsValid()) {
    return false;
  }
  webview_->OpenDevToolsWindow();
  return true;
}

wil::com_ptr<ICoreWebView2CookieManager> Webview::GetCookieManager() {
  wil::com_ptr<ICoreWebView2_2> webview2 =
      webview_.try_query<ICoreWebView2_2>();
  if (!webview2) {
    return nullptr;
  }

  wil::com_ptr<ICoreWebView2CookieManager> cookie_manager;
  if (FAILED(webview2->get_CookieManager(cookie_manager.put()))) {
    return nullptr;
  }
  return cookie_manager;
}

void Webview::ClearCookies(CookiesClearedCallback callback) {
  if (!IsValid()) {
    callback(false, false);
    return;
  }

  auto cookie_manager = GetCookieManager();
  if (!cookie_manager) {
    callback(false, false);
    return;
  }

  cookie_manager->GetCookies(
      nullptr, Callback<ICoreWebView2GetCookiesCompletedHandler>(
                   [cookie_manager, callback = std::move(callback)](
                       HRESULT error_code,
                       ICoreWebView2CookieList *cookie_list) -> HRESULT {
                     UINT cookie_count = 0;
                     if (SUCCEEDED(error_code) && cookie_list) {
                       cookie_list->get_Count(&cookie_count);
                     }

                     const HRESULT delete_result =
                         cookie_manager->DeleteAllCookies();
                     callback(SUCCEEDED(error_code) && SUCCEEDED(delete_result),
                              cookie_count > 0);
                     return S_OK;
                   })
                   .Get());
}

bool Webview::SetCookie(const WebviewCookie &cookie) {
  if (!IsValid()) {
    return false;
  }

  auto cookie_manager = GetCookieManager();
  if (!cookie_manager) {
    return false;
  }

  wil::com_ptr<ICoreWebView2Cookie> native_cookie;
  if (!CreateNativeCookie(cookie_manager.get(), cookie, native_cookie)) {
    return false;
  }

  return SUCCEEDED(cookie_manager->AddOrUpdateCookie(native_cookie.get()));
}

void Webview::GetCookies(const std::string &url,
                         CookiesRetrievedCallback callback) {
  if (!IsValid()) {
    callback(false, {});
    return;
  }

  auto cookie_manager = GetCookieManager();
  if (!cookie_manager) {
    callback(false, {});
    return;
  }

  const std::wstring url_utf16 = util::Utf16FromUtf8(url);
  cookie_manager->GetCookies(
      url_utf16.c_str(),
      Callback<ICoreWebView2GetCookiesCompletedHandler>(
          [callback = std::move(callback)](
              HRESULT error_code,
              ICoreWebView2CookieList *cookie_list) -> HRESULT {
            if (FAILED(error_code) || !cookie_list) {
              callback(false, {});
              return S_OK;
            }

            UINT cookie_count = 0;
            if (FAILED(cookie_list->get_Count(&cookie_count))) {
              callback(false, {});
              return S_OK;
            }

            std::vector<WebviewCookie> cookies;
            cookies.reserve(cookie_count);
            for (UINT index = 0; index < cookie_count; index++) {
              wil::com_ptr<ICoreWebView2Cookie> native_cookie;
              if (FAILED(cookie_list->GetValueAtIndex(index,
                                                      native_cookie.put()))) {
                callback(false, {});
                return S_OK;
              }

              WebviewCookie cookie;
              if (!ReadCookieString(native_cookie.get(),
                                    &ICoreWebView2Cookie::get_Name,
                                    cookie.name) ||
                  !ReadCookieString(native_cookie.get(),
                                    &ICoreWebView2Cookie::get_Value,
                                    cookie.value) ||
                  !ReadCookieString(native_cookie.get(),
                                    &ICoreWebView2Cookie::get_Domain,
                                    cookie.domain) ||
                  !ReadCookieString(native_cookie.get(),
                                    &ICoreWebView2Cookie::get_Path,
                                    cookie.path)) {
                callback(false, {});
                return S_OK;
              }

              double expires = 0;
              BOOL is_http_only = false;
              BOOL is_secure = false;
              COREWEBVIEW2_COOKIE_SAME_SITE_KIND same_site =
                  COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE;
              BOOL is_session = false;
              if (FAILED(native_cookie->get_Expires(&expires)) ||
                  FAILED(native_cookie->get_IsHttpOnly(&is_http_only)) ||
                  FAILED(native_cookie->get_IsSecure(&is_secure)) ||
                  FAILED(native_cookie->get_SameSite(&same_site)) ||
                  FAILED(native_cookie->get_IsSession(&is_session))) {
                callback(false, {});
                return S_OK;
              }
              cookie.expires = expires;
              cookie.is_http_only = is_http_only;
              cookie.is_secure = is_secure;
              cookie.same_site = static_cast<int64_t>(same_site);
              cookie.is_session = is_session;
              cookies.push_back(std::move(cookie));
            }

            callback(true, std::move(cookies));
            return S_OK;
          })
          .Get());
}

bool Webview::DeleteCookie(const WebviewCookie &cookie) {
  if (!IsValid()) {
    return false;
  }

  auto cookie_manager = GetCookieManager();
  if (!cookie_manager) {
    return false;
  }

  wil::com_ptr<ICoreWebView2Cookie> native_cookie;
  if (!CreateNativeCookie(cookie_manager.get(), cookie, native_cookie)) {
    return false;
  }

  return SUCCEEDED(cookie_manager->DeleteCookie(native_cookie.get()));
}

bool Webview::DeleteCookiesWithNameAndUrl(const std::string &name,
                                          const std::string &url) {
  if (!IsValid()) {
    return false;
  }

  auto cookie_manager = GetCookieManager();
  if (!cookie_manager) {
    return false;
  }

  const std::wstring name_utf16 = util::Utf16FromUtf8(name);
  const std::wstring url_utf16 = util::Utf16FromUtf8(url);
  return SUCCEEDED(
      cookie_manager->DeleteCookies(name_utf16.c_str(), url_utf16.c_str()));
}

bool Webview::DeleteCookiesWithNameDomainAndPath(const std::string &name,
                                                 const std::string &domain,
                                                 const std::string &path) {
  if (!IsValid()) {
    return false;
  }

  auto cookie_manager = GetCookieManager();
  if (!cookie_manager) {
    return false;
  }

  const std::wstring name_utf16 = util::Utf16FromUtf8(name);
  const std::wstring domain_utf16 = util::Utf16FromUtf8(domain);
  const std::wstring path_utf16 = util::Utf16FromUtf8(path);
  return SUCCEEDED(cookie_manager->DeleteCookiesWithDomainAndPath(
      name_utf16.c_str(), domain_utf16.c_str(), path_utf16.c_str()));
}

bool Webview::ClearCache() {
  if (!IsValid()) {
    return false;
  }
  return webview_->CallDevToolsProtocolMethod(L"Network.clearBrowserCache",
                                              L"{}", nullptr) == S_OK;
}

void Webview::ClearLocalStorage(OperationCompletedCallback callback) {
  if (!IsValid()) {
    callback(false);
    return;
  }

  auto webview13 = webview_.try_query<ICoreWebView2_13>();
  if (!webview13) {
    callback(false);
    return;
  }

  wil::com_ptr<ICoreWebView2Profile> profile;
  if (FAILED(webview13->get_Profile(profile.put())) || !profile) {
    callback(false);
    return;
  }

  auto profile2 = profile.try_query<ICoreWebView2Profile2>();
  if (!profile2) {
    callback(false);
    return;
  }

  auto completion =
      std::make_shared<OperationCompletedCallback>(std::move(callback));
  HRESULT hr = profile2->ClearBrowsingData(
      COREWEBVIEW2_BROWSING_DATA_KINDS_ALL_DOM_STORAGE,
      Callback<ICoreWebView2ClearBrowsingDataCompletedHandler>(
          [completion](HRESULT error_code) -> HRESULT {
            (*completion)(SUCCEEDED(error_code));
            return S_OK;
          })
          .Get());
  if (FAILED(hr)) {
    (*completion)(false);
  }
}

bool Webview::SetCacheDisabled(bool disabled) {
  if (!IsValid()) {
    return false;
  }
  const std::string json =
      disabled ? R"({"disableCache":true})" : R"({"disableCache":false})";
  return webview_->CallDevToolsProtocolMethod(L"Network.setCacheDisabled",
                                              util::Utf16FromUtf8(json).c_str(),
                                              nullptr) == S_OK;
}

void Webview::SetPopupWindowPolicy(WebviewPopupWindowPolicy policy) {
  popup_window_policy_ = policy;
}

void Webview::SetNavigationRequestCallbacksEnabled(bool enabled) {
  navigation_request_callbacks_enabled_ = enabled;
  InvalidatePendingNavigationRequests();
}

void Webview::SetJavaScriptDialogCallbacksEnabled(bool alert, bool confirm,
                                                  bool prompt) {
  java_script_alert_dialog_enabled_ = alert;
  java_script_confirm_dialog_enabled_ = confirm;
  java_script_prompt_dialog_enabled_ = prompt;
}

void Webview::InvalidatePendingNavigationRequests() {
  ++latest_navigation_request_id_;
  approved_navigation_urls_.clear();
  bypass_next_navigation_count_ = 0;
}

void Webview::MarkNavigationApproved(const std::string &url) {
  approved_navigation_urls_.insert(CanonicalizeNavigationUrl(url));
}

bool Webview::ConsumeApprovedNavigation(const std::string &url) {
  const auto approved =
      approved_navigation_urls_.find(CanonicalizeNavigationUrl(url));
  if (approved != approved_navigation_urls_.end()) {
    approved_navigation_urls_.erase(approved);
    return true;
  }
  if (bypass_next_navigation_count_ > 0) {
    --bypass_next_navigation_count_;
    return true;
  }
  return false;
}

void Webview::ResumeNavigation(uint64_t request_id, const std::string &url) {
  if (!IsValid() || request_id != latest_navigation_request_id_) {
    return;
  }

  MarkNavigationApproved(url);
  if (FAILED(webview_->Navigate(util::Utf16FromUtf8(url).c_str()))) {
    ConsumeApprovedNavigation(url);
  }
}

bool Webview::SetUserAgent(const std::string *user_agent) {
  if (settings2_) {
    if (user_agent == nullptr && default_user_agent_.empty()) {
      return false;
    }
    const std::string &resolved_user_agent =
        user_agent != nullptr ? *user_agent : default_user_agent_;
    return settings2_->put_UserAgent(
               util::Utf16FromUtf8(resolved_user_agent).c_str()) == S_OK;
  }
  return false;
}

std::optional<std::string> Webview::GetUserAgent() {
  if (!settings2_) {
    return std::nullopt;
  }

  wil::unique_cotaskmem_string user_agent;
  if (FAILED(settings2_->get_UserAgent(&user_agent)) || user_agent == nullptr) {
    return std::nullopt;
  }
  return util::Utf8FromUtf16(user_agent.get());
}

bool Webview::SetJavaScriptEnabled(bool enabled) {
  if (settings_) {
    return settings_->put_IsScriptEnabled(enabled ? TRUE : FALSE) == S_OK;
  }
  return false;
}

bool Webview::SetZoomControlEnabled(bool enabled) {
  if (settings_) {
    return settings_->put_IsZoomControlEnabled(enabled ? TRUE : FALSE) == S_OK;
  }
  return false;
}

bool Webview::SetBackgroundColor(int32_t color) {
  if (!IsValid()) {
    return false;
  }

  COREWEBVIEW2_COLOR webview_color;
  ConvertColor(webview_color, color);

  // Semi-transparent backgrounds are not supported.
  // Valid alpha values are 0 or 255.
  if (webview_color.A > 0) {
    webview_color.A = 0xFF;
  }

  return webview_controller_->put_DefaultBackgroundColor(webview_color) == S_OK;
}

bool Webview::SetZoomFactor(double factor) {
  if (!IsValid()) {
    return false;
  }
  return webview_controller_->put_ZoomFactor(factor) == S_OK;
}

void Webview::SetCursorPos(double x, double y) {
  if (!IsValid()) {
    return;
  }

  POINT point;
  point.x = static_cast<LONG>(x * scale_factor_);
  point.y = static_cast<LONG>(y * scale_factor_);
  last_cursor_pos_ = point;

  // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2?view=webview2-1.0.774.44
  composition_controller_->SendMouseInput(
      COREWEBVIEW2_MOUSE_EVENT_KIND::COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE,
      virtual_keys_.state(), 0, point);
}

void Webview::SetPointerUpdate(int32_t pointer,
                               WebviewPointerEventKind eventKind, double x,
                               double y, double size, double pressure) {
  if (!IsValid()) {
    return;
  }

  COREWEBVIEW2_POINTER_EVENT_KIND event =
      COREWEBVIEW2_POINTER_EVENT_KIND_UPDATE;
  UINT32 pointerFlags = POINTER_FLAG_NONE;
  switch (eventKind) {
  case WebviewPointerEventKind::Activate:
    event = COREWEBVIEW2_POINTER_EVENT_KIND_ACTIVATE;
    break;
  case WebviewPointerEventKind::Down:
    event = COREWEBVIEW2_POINTER_EVENT_KIND_DOWN;
    pointerFlags =
        POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
    break;
  case WebviewPointerEventKind::Enter:
    event = COREWEBVIEW2_POINTER_EVENT_KIND_ENTER;
    break;
  case WebviewPointerEventKind::Leave:
    event = COREWEBVIEW2_POINTER_EVENT_KIND_LEAVE;
    break;
  case WebviewPointerEventKind::Up:
    event = COREWEBVIEW2_POINTER_EVENT_KIND_UP;
    pointerFlags = POINTER_FLAG_UP;
    break;
  case WebviewPointerEventKind::Update:
    event = COREWEBVIEW2_POINTER_EVENT_KIND_UPDATE;
    pointerFlags =
        POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
    break;
  }

  POINT point;
  point.x = static_cast<LONG>(x * scale_factor_);
  point.y = static_cast<LONG>(y * scale_factor_);

  RECT rect;
  rect.left = point.x - 2;
  rect.right = point.x + 2;
  rect.top = point.y - 2;
  rect.bottom = point.y + 2;

  host_->CreateWebViewPointerInfo(
      [this, pointer, event, pointerFlags, point, rect,
       pressure](wil::com_ptr<ICoreWebView2PointerInfo> pointerInfo,
                 std::unique_ptr<WebviewCreationError> error) {
        if (pointerInfo) {
          ICoreWebView2PointerInfo *pInfo = pointerInfo.get();
          pInfo->put_PointerId(pointer);
          pInfo->put_PointerKind(PT_TOUCH);
          pInfo->put_PointerFlags(pointerFlags);
          pInfo->put_TouchFlags(TOUCH_FLAG_NONE);
          pInfo->put_TouchMask(TOUCH_MASK_CONTACTAREA | TOUCH_MASK_PRESSURE);
          pInfo->put_TouchPressure(
              std::clamp((UINT32)(pressure == 0.0 ? 1024 : 1024 * pressure),
                         (UINT32)0, (UINT32)1024));
          pInfo->put_PixelLocationRaw(point);
          pInfo->put_TouchContactRaw(rect);
          composition_controller_->SendPointerInput(event, pInfo);
        }
      });
}

void Webview::SetPointerButtonState(WebviewPointerButton button, bool is_down) {
  if (!IsValid()) {
    return;
  }

  COREWEBVIEW2_MOUSE_EVENT_KIND kind;
  switch (button) {
  case WebviewPointerButton::Primary:
    virtual_keys_.set_isLeftButtonDown(is_down);
    kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN
                   : COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP;
    break;
  case WebviewPointerButton::Secondary:
    virtual_keys_.set_isRightButtonDown(is_down);
    kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN
                   : COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP;
    break;
  case WebviewPointerButton::Tertiary:
    virtual_keys_.set_isMiddleButtonDown(is_down);
    kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN
                   : COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP;
    break;
  default:
    kind = static_cast<COREWEBVIEW2_MOUSE_EVENT_KIND>(0);
  }

  composition_controller_->SendMouseInput(kind, virtual_keys_.state(), 0,
                                          last_cursor_pos_);
}

void Webview::SendScroll(double delta, bool horizontal) {
  auto &remainder =
      horizontal ? horizontal_scroll_remainder_ : vertical_scroll_remainder_;
  const auto native_delta =
      (delta / GetFlutterScrollOffsetMultiplier()) * WHEEL_DELTA + remainder;
  auto wheel_delta = static_cast<long>(std::round(native_delta));
  remainder = native_delta - static_cast<double>(wheel_delta);

  if (wheel_delta == 0) {
    return;
  }

  wheel_delta =
      std::clamp<long>(wheel_delta, (std::numeric_limits<short>::min)(),
                       (std::numeric_limits<short>::max)());
  auto offset = static_cast<short>(wheel_delta);

  if (horizontal) {
    composition_controller_->SendMouseInput(
        COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL, virtual_keys_.state(),
        offset, last_cursor_pos_);
  } else {
    composition_controller_->SendMouseInput(COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL,
                                            virtual_keys_.state(), offset,
                                            last_cursor_pos_);
  }
}

void Webview::SetScrollDelta(double delta_x, double delta_y) {
  if (!IsValid()) {
    return;
  }

  if (delta_x != 0.0) {
    SendScroll(delta_x, true);
  }
  if (delta_y != 0.0) {
    SendScroll(delta_y, false);
  }
}

void Webview::LoadUrl(const std::string &url) {
  if (IsValid()) {
    InvalidatePendingNavigationRequests();
    MarkNavigationApproved(url);
    if (FAILED(webview_->Navigate(util::Utf16FromUtf8(url).c_str()))) {
      ConsumeApprovedNavigation(url);
    }
  }
}

bool Webview::LoadRequest(const std::string &url, const std::string &method,
                          const std::string &headers,
                          const std::vector<uint8_t> *body) {
  if (!IsValid()) {
    return false;
  }

  auto webview2 = webview_.try_query<ICoreWebView2_2>();
  if (!webview2) {
    return false;
  }

  wil::com_ptr<ICoreWebView2WebResourceRequest> request =
      host_->CreateWebResourceRequest(url, method, headers, body);
  if (!request) {
    return false;
  }

  InvalidatePendingNavigationRequests();
  MarkNavigationApproved(url);
  if (FAILED(webview2->NavigateWithWebResourceRequest(request.get()))) {
    ConsumeApprovedNavigation(url);
    return false;
  }
  return true;
}

void Webview::LoadStringContent(const std::string &content) {
  if (IsValid()) {
    InvalidatePendingNavigationRequests();
    constexpr auto kStringContentNavigationUrl = "about:blank";
    MarkNavigationApproved(kStringContentNavigationUrl);
    if (FAILED(
            webview_->NavigateToString(util::Utf16FromUtf8(content).c_str()))) {
      ConsumeApprovedNavigation(kStringContentNavigationUrl);
    }
  }
}

bool Webview::Stop() {
  if (!IsValid()) {
    return false;
  }
  return SUCCEEDED(webview_->CallDevToolsProtocolMethod(L"Page.stopLoading",
                                                        L"{}", nullptr));
}

bool Webview::Reload() {
  if (!IsValid()) {
    return false;
  }
  InvalidatePendingNavigationRequests();
  ++bypass_next_navigation_count_;
  if (FAILED(webview_->Reload())) {
    --bypass_next_navigation_count_;
    return false;
  }
  return true;
}

bool Webview::GoBack() {
  if (!IsValid()) {
    return false;
  }
  BOOL can_go_back = FALSE;
  if (FAILED(webview_->get_CanGoBack(&can_go_back)) || can_go_back != TRUE) {
    return false;
  }
  InvalidatePendingNavigationRequests();
  ++bypass_next_navigation_count_;
  if (FAILED(webview_->GoBack())) {
    --bypass_next_navigation_count_;
    return false;
  }
  return true;
}

bool Webview::GoForward() {
  if (!IsValid()) {
    return false;
  }
  BOOL can_go_forward = FALSE;
  if (FAILED(webview_->get_CanGoForward(&can_go_forward)) ||
      can_go_forward != TRUE) {
    return false;
  }
  InvalidatePendingNavigationRequests();
  ++bypass_next_navigation_count_;
  if (FAILED(webview_->GoForward())) {
    --bypass_next_navigation_count_;
    return false;
  }
  return true;
}

void Webview::AddScriptToExecuteOnDocumentCreated(
    const std::string &script,
    AddScriptToExecuteOnDocumentCreatedCallback callback) {
  if (IsValid()) {
    if (SUCCEEDED(webview_->AddScriptToExecuteOnDocumentCreated(
            util::Utf16FromUtf8(script).c_str(),
            Callback<
                ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>(
                [callback](HRESULT result, LPCWSTR wsid) -> HRESULT {
                  std::string sid = util::Utf8FromUtf16(wsid);
                  callback(SUCCEEDED(result), sid);
                  return S_OK;
                })
                .Get()))) {
      return;
    }
  }

  callback(false, std::string());
}

void Webview::RemoveScriptToExecuteOnDocumentCreated(
    const std::string &script_id) {
  if (IsValid()) {
    webview_->RemoveScriptToExecuteOnDocumentCreated(
        util::Utf16FromUtf8(script_id).c_str());
  }
}

void Webview::ExecuteScript(const std::string &script,
                            ScriptExecutedCallback callback) {
  if (IsValid()) {
    if (SUCCEEDED(webview_->ExecuteScript(
            util::Utf16FromUtf8(script).c_str(),
            Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
                [callback](HRESULT result, LPCWSTR json_result_object) {
                  callback(SUCCEEDED(result),
                           util::Utf8FromUtf16(json_result_object));
                  return S_OK;
                })
                .Get()))) {
      return;
    }
  }

  callback(false, std::string());
}

void Webview::ReleaseFocus(ScriptExecutedCallback callback) {
  ExecuteScript(
      kReleaseWebInteractionScript,
      [this, callback = std::move(callback)](bool success,
                                             const std::string &result) {
        SetFocus(hwnd_);
        callback(success, result);
      });
}

bool Webview::PostWebMessage(const std::string &json) {
  if (!IsValid()) {
    return false;
  }
  return webview_->PostWebMessageAsJson(util::Utf16FromUtf8(json).c_str()) ==
         S_OK;
}

bool Webview::ConfigureTrusted(const std::string &origin,
                                const std::string &controller_id,
                                const std::string &bridge_token,
                                const std::string &initial_url,
                                bool diagnostics_enabled,
                                bool show_scrollbars,
                                std::string *failure_reason) {
  const auto fail = [failure_reason](const char *reason) {
    if (failure_reason != nullptr) *failure_reason = reason;
    return false;
  };
  if (!IsValid() || trusted_configured_ || origin.rfind("http://localhost:", 0) != 0 ||
      controller_id.empty() || bridge_token.empty()) {
    return fail("The WebView or loopback identifiers are invalid.");
  }
  if (!trusted_document_request_filter_ready_) {
    return fail("Document request filtering is unavailable.");
  }
  trusted_configured_ = true;
  trusted_origin_ = origin;
  if (!IsTrustedUrl(initial_url)) {
    trusted_configured_ = false;
    trusted_origin_.clear();
    return fail("The initial URL is outside the loopback origin.");
  }
  trusted_controller_id_ = controller_id;
  trusted_bridge_token_ = bridge_token;
  trusted_initial_url_ = initial_url;

  const std::string script =
      "(()=>{if(globalThis.top!==globalThis||location.origin!==\"" + origin +
      "\")return;const token=\"" + bridge_token +
      "\";const send=(channelName,message)=>chrome.webview.postMessage({"
      "coreframeToken:token,coreframeType:'channel',channelName,message});"
      "Object.defineProperty(globalThis,'CoreframeFlutter',{value:{"
      "postMessage:message=>send('CoreframeFlutter',String(message))},"
      "configurable:false,writable:false});"
      "let selectionActive=false;const selection=()=>{const selected=globalThis.getSelection?.();"
      "const element=document.activeElement;const editable=element instanceof HTMLInputElement||"
      "element instanceof HTMLTextAreaElement;const next=Boolean((selected&&selected.rangeCount>0&&"
      "!selected.isCollapsed)||(editable&&element.selectionStart!==null&&element.selectionEnd!==null&&"
      "element.selectionStart!==element.selectionEnd));if(next===selectionActive)return;"
      "selectionActive=next;send('CoreframeFlutterTextSelection',next)};"
      "document.addEventListener('selectionchange',selection,true);"
      "document.addEventListener('focusin',selection,true);"
      "document.addEventListener('focusout',selection,true);selection();" +
      "document.addEventListener('focusin',()=>send('CoreframeFlutterFocus',true),true);" +
      (diagnostics_enabled
           ? "for(const level of ['debug','error','info','log','warn']){"
             "const original=console[level]?.bind(console);if(!original)continue;"
             "console[level]=(...values)=>{original(...values);try{chrome.webview.postMessage({"
             "coreframeToken:token,coreframeType:'console',level:level==='warn'?'warning':level,"
             "message:values.map(value=>{try{return typeof value==='string'?value:JSON.stringify(value)}"
             "catch(_){return String(value)}}).join(' ')})}catch{}}}"
           : std::string()) +
      (show_scrollbars
           ? std::string()
           : "const style=document.createElement('style');style.textContent='"
             "::-webkit-scrollbar{display:none!important}';"
             "(document.head||document.documentElement).appendChild(style);") +
      "})();";
  const std::weak_ptr<LifetimeState> weak_state = lifetime_state_;
  AddScriptToExecuteOnDocumentCreated(
      script, [weak_state](bool success, const std::string &) {
        const std::shared_ptr<LifetimeState> state = weak_state.lock();
        if (!state || !state->owner) return;
        if (success) {
          state->owner->trusted_bridge_script_ready_ = true;
          if (state->owner->trusted_start_requested_) {
            state->owner->NavigateTrustedWithKind(
                state->owner->trusted_initial_url_, "initial");
          }
        } else if (state->owner->trusted_security_event_callback_) {
          state->owner->trusted_security_event_callback_(
              "loadError", "", "Bridge injection failed.",
              state->owner->trusted_epoch_);
        }
      });
  return true;
}

bool Webview::StartTrusted() {
  if (!trusted_configured_ || trusted_initial_started_) return false;
  trusted_initial_started_ = true;
  trusted_start_requested_ = true;
  return !trusted_bridge_script_ready_ ||
         NavigateTrustedWithKind(trusted_initial_url_, "initial");
}

bool Webview::LoadTrusted(const std::string &url) {
  if (!trusted_configured_ || !trusted_ready_ || !IsTrustedUrl(url)) return false;
  return NavigateTrustedWithKind(url, "explicit");
}

bool Webview::GoBackTrusted() {
  if (!trusted_configured_ || !trusted_ready_) return false;
  BOOL can_go_back = FALSE;
  if (FAILED(webview_->get_CanGoBack(&can_go_back)) || can_go_back != TRUE) {
    return false;
  }
  trusted_next_navigation_kind_ = "history";
  trusted_next_navigation_action_ = "back";
  trusted_next_navigation_url_.clear();
  trusted_next_navigation_deadline_ =
      std::chrono::steady_clock::now() + std::chrono::seconds(5);
  if (SUCCEEDED(webview_->GoBack())) return true;
  trusted_next_navigation_kind_.clear();
  trusted_next_navigation_action_.clear();
  return false;
}

bool Webview::GoForwardTrusted() {
  if (!trusted_configured_ || !trusted_ready_) return false;
  BOOL can_go_forward = FALSE;
  if (FAILED(webview_->get_CanGoForward(&can_go_forward)) ||
      can_go_forward != TRUE) {
    return false;
  }
  trusted_next_navigation_kind_ = "history";
  trusted_next_navigation_action_ = "forward";
  trusted_next_navigation_url_.clear();
  trusted_next_navigation_deadline_ =
      std::chrono::steady_clock::now() + std::chrono::seconds(5);
  if (SUCCEEDED(webview_->GoForward())) return true;
  trusted_next_navigation_kind_.clear();
  trusted_next_navigation_action_.clear();
  return false;
}

bool Webview::ReloadTrusted() {
  if (!trusted_configured_ || !trusted_ready_) return false;
  trusted_next_navigation_kind_ = "reload";
  trusted_next_navigation_action_ = "reload";
  trusted_next_navigation_url_.clear();
  trusted_next_navigation_deadline_ =
      std::chrono::steady_clock::now() + std::chrono::seconds(5);
  if (SUCCEEDED(webview_->Reload())) return true;
  trusted_next_navigation_kind_.clear();
  trusted_next_navigation_action_.clear();
  return false;
}

bool Webview::NavigateTrustedWithKind(const std::string &url,
                                       const std::string &kind) {
  if (!trusted_configured_ || !IsTrustedUrl(url) ||
      !trusted_pending_navigations_.empty()) {
    return false;
  }
  trusted_next_navigation_kind_ = kind;
  trusted_next_navigation_action_ = "navigate";
  trusted_next_navigation_url_ = url;
  trusted_next_navigation_deadline_ =
      std::chrono::steady_clock::now() + std::chrono::seconds(5);
  if (FAILED(webview_->Navigate(util::Utf16FromUtf8(url).c_str()))) {
    trusted_next_navigation_kind_.clear();
    trusted_next_navigation_action_.clear();
    trusted_next_navigation_url_.clear();
    return false;
  }
  return true;
}

std::string Webview::ConsumeTrustedNavigationKind(
    const std::string &url, bool is_redirected) {
  if (is_redirected) {
    trusted_next_navigation_kind_.clear();
    trusted_next_navigation_action_.clear();
    trusted_next_navigation_url_.clear();
    trusted_consumed_navigation_action_ = "navigate";
    return "redirect";
  }
  const bool current = !trusted_next_navigation_kind_.empty() &&
                       std::chrono::steady_clock::now() <=
                           trusted_next_navigation_deadline_ &&
                       (trusted_next_navigation_url_.empty() ||
                        CanonicalizeNavigationUrl(url) ==
                            CanonicalizeNavigationUrl(
                                trusted_next_navigation_url_));
  std::string kind = current ? trusted_next_navigation_kind_ : "other";
  trusted_consumed_navigation_action_ =
      current ? trusted_next_navigation_action_ : "navigate";
  trusted_next_navigation_kind_.clear();
  trusted_next_navigation_action_.clear();
  trusted_next_navigation_url_.clear();
  return kind;
}

void Webview::BeginTrustedNavigation(
    ICoreWebView2NavigationStartingEventArgs *args, const std::string &url,
    const std::string &frame, const std::string &kind,
    bool is_user_initiated, bool restore_active, bool restore_ready,
    const std::string &restore_url) {
  if (args == nullptr || trusted_navigation_request_callback_ == nullptr) {
    if (args != nullptr) args->put_Cancel(TRUE);
    return;
  }
  // NavigationStarting has no deferral in the pinned Win32 SDK. Cancel now,
  // then replay one matching engine operation only after Dart authorizes it.
  if (FAILED(args->put_Cancel(TRUE))) return;
  const std::string request_id =
      trusted_controller_id_ + "-" +
      std::to_string(++trusted_request_sequence_);
  PendingTrustedNavigation pending;
  pending.request_id = request_id;
  pending.url = url;
  pending.frame = frame;
  pending.kind = kind;
  pending.resume_action = trusted_consumed_navigation_action_;
  trusted_consumed_navigation_action_.clear();
  pending.epoch = trusted_epoch_;
  pending.is_user_initiated = is_user_initiated;
  pending.restore_active = restore_active;
  pending.restore_ready = restore_ready;
  pending.restore_url = restore_url;
  const std::weak_ptr<LifetimeState> weak_state = lifetime_state_;
  pending.timeout_id = ScheduleDecisionTimeout([weak_state, request_id]() {
    const auto state = weak_state.lock();
    if (state && state->owner) {
      state->owner->ExpireTrustedNavigation(request_id);
    }
  });
  if (pending.timeout_id == 0) {
    return;
  }
  trusted_pending_navigations_.emplace(request_id, std::move(pending));
  trusted_navigation_request_callback_(request_id, url, frame, kind,
                                        is_user_initiated,
                                        trusted_epoch_);
}

void Webview::BeginTrustedPopup(
    ICoreWebView2NewWindowRequestedEventArgs *args, const std::string &url,
    bool is_user_initiated) {
  if (args == nullptr || trusted_navigation_request_callback_ == nullptr) {
    if (args != nullptr) args->put_Handled(TRUE);
    return;
  }
  wil::com_ptr<ICoreWebView2Deferral> deferral;
  if (FAILED(args->GetDeferral(deferral.put())) || !deferral) {
    args->put_Handled(TRUE);
    return;
  }
  const std::string request_id =
      trusted_controller_id_ + "-" +
      std::to_string(++trusted_request_sequence_);
  PendingTrustedNavigation pending;
  pending.request_id = request_id;
  pending.url = url;
  pending.frame = "main";
  pending.kind = "popup";
  pending.epoch = trusted_epoch_;
  pending.is_user_initiated = is_user_initiated;
  pending.deferral = std::move(deferral);
  args->AddRef();
  pending.popup_args.attach(args);
  const std::weak_ptr<LifetimeState> weak_state = lifetime_state_;
  pending.timeout_id = ScheduleDecisionTimeout([weak_state, request_id]() {
    const auto state = weak_state.lock();
    if (state && state->owner) {
      state->owner->ExpireTrustedNavigation(request_id);
    }
  });
  if (pending.timeout_id == 0) {
    args->put_Handled(TRUE);
    pending.deferral->Complete();
    return;
  }
  trusted_pending_navigations_.emplace(request_id, std::move(pending));
  trusted_navigation_request_callback_(request_id, url, "main", "popup",
                                        is_user_initiated,
                                        trusted_epoch_);
}

void Webview::RestoreTrustedDocument(
    const PendingTrustedNavigation &pending) {
  if (!pending.restore_active || !pending.restore_ready ||
      pending.restore_url.empty() || !IsTrustedUrl(pending.restore_url)) {
    return;
  }
  // BeginTrustedNavigation cancels NavigationStarting before the pending
  // request is stored. A pending denial therefore means WebView2 retained the
  // previous document; restore its trust under the new monotonic epoch.
  trusted_authorized_url_ = pending.restore_url;
  trusted_active_ = true;
  trusted_ready_ = true;
  if (trusted_security_event_callback_) {
    trusted_security_event_callback_("documentRestored", pending.restore_url,
                                      "", trusted_epoch_);
  }
}

bool Webview::CompleteTrustedNavigation(const std::string &request_id,
                                          const std::string &controller_id,
                                          uint64_t epoch,
                                          const std::string &disposition) {
  auto found = trusted_pending_navigations_.find(request_id);
  if (found == trusted_pending_navigations_.end() ||
      controller_id != trusted_controller_id_ ||
      epoch != found->second.epoch || epoch != trusted_epoch_ ||
      (disposition != "allow" && disposition != "block" &&
       disposition != "external")) {
    return false;
  }
  PendingTrustedNavigation pending = std::move(found->second);
  trusted_pending_navigations_.erase(found);
  CancelDecisionTimeout(pending.timeout_id);
  const bool allow = disposition == "allow" && !pending.popup_args &&
                     pending.frame == "main" &&
                     IsTrustedUrl(pending.url);
  const bool external = disposition == "external" &&
                        pending.is_user_initiated &&
                        pending.frame == "main" &&
                        IsExternalHttpUrl(pending.url);
  if (pending.popup_args) pending.popup_args->put_Handled(TRUE);
  if (external) {
    ShellExecuteW(nullptr, L"open", util::Utf16FromUtf8(pending.url).c_str(),
                  nullptr, nullptr, SW_SHOWNORMAL);
  }
  if (!allow) RestoreTrustedDocument(pending);
  if (!allow && trusted_security_event_callback_) {
    trusted_security_event_callback_("navigationBlocked", pending.url,
                                       external ? "external" : pending.kind,
                                       pending.epoch);
  }
  if (pending.deferral) pending.deferral->Complete();
  return !allow || ReplayTrustedNavigation(
                       pending.url, pending.epoch, pending.resume_action);
}

void Webview::ExpireTrustedNavigation(const std::string &request_id) {
  auto found = trusted_pending_navigations_.find(request_id);
  if (found == trusted_pending_navigations_.end()) return;
  PendingTrustedNavigation pending = std::move(found->second);
  trusted_pending_navigations_.erase(found);
  RestoreTrustedDocument(pending);
  if (pending.popup_args) pending.popup_args->put_Handled(TRUE);
  if (trusted_security_event_callback_) {
    trusted_security_event_callback_("navigationBlocked", pending.url,
                                       "timeout", pending.epoch);
  }
  if (pending.deferral) pending.deferral->Complete();
}

void Webview::CancelTrustedNavigations() {
  for (auto &[_, pending] : trusted_pending_navigations_) {
    CancelDecisionTimeout(pending.timeout_id);
    if (pending.popup_args) pending.popup_args->put_Handled(TRUE);
    if (pending.deferral) pending.deferral->Complete();
  }
  trusted_pending_navigations_.clear();
  trusted_replay_url_.clear();
  trusted_replay_epoch_ = 0;
}

bool Webview::ReplayTrustedNavigation(const std::string &url, uint64_t epoch,
                                       const std::string &action) {
  if (!trusted_configured_ || epoch != trusted_epoch_ || !IsTrustedUrl(url) ||
      !trusted_replay_url_.empty()) {
    return false;
  }
  trusted_replay_url_ = url;
  trusted_replay_epoch_ = epoch;
  HRESULT result = E_FAIL;
  if (action == "back") {
    result = webview_->GoBack();
  } else if (action == "forward") {
    result = webview_->GoForward();
  } else if (action == "reload") {
    result = webview_->Reload();
  } else {
    result = webview_->Navigate(util::Utf16FromUtf8(url).c_str());
  }
  if (FAILED(result)) {
    trusted_replay_url_.clear();
    trusted_replay_epoch_ = 0;
    return false;
  }
  return true;
}

bool Webview::ConsumeTrustedReplay(const std::string &url,
                                    uint64_t navigation_id) {
  if (trusted_replay_url_.empty() ||
      trusted_replay_epoch_ != trusted_epoch_ ||
      CanonicalizeNavigationUrl(url) !=
          CanonicalizeNavigationUrl(trusted_replay_url_)) {
    return false;
  }
  trusted_authorized_url_ = url;
  trusted_navigation_id_ = navigation_id;
  trusted_replay_url_.clear();
  trusted_replay_epoch_ = 0;
  return true;
}

bool Webview::MarkTrustedReady(const std::string &controller_id,
                                const std::string &origin, uint64_t epoch) {
  if (!trusted_active_ || controller_id != trusted_controller_id_ ||
      origin != trusted_origin_ || epoch != trusted_epoch_) {
    return false;
  }
  trusted_ready_ = true;
  return true;
}

void Webview::DispatchTrusted(const std::string &controller_id,
                               const std::string &origin, uint64_t epoch,
                               const std::string &script,
                               OperationCompletedCallback callback) {
  if (!trusted_active_ || controller_id != trusted_controller_id_ ||
      origin != trusted_origin_ || epoch != trusted_epoch_) {
    callback(false);
    return;
  }
  const std::string guarded_script =
      "(()=>{if(globalThis.top!==globalThis||location.origin!==" +
      JavaScriptStringLiteral(trusted_origin_) +
      ")throw new Error('Untrusted trusted document');" + script +
      ";return true;})()";
  trusted_dispatch_in_progress_ = true;
  const std::weak_ptr<LifetimeState> weak_state = lifetime_state_;
  ExecuteScript(guarded_script, [weak_state, callback](
                                    bool success, const std::string &) {
    const std::shared_ptr<LifetimeState> state = weak_state.lock();
    if (state && state->owner) {
      state->owner->trusted_dispatch_in_progress_ = false;
    }
    callback(success && state && state->owner);
  });
}

void Webview::CapturePresentationArtifact(
    const PresentationIdentity &expected, PresentationCaptureCallback callback) {
  if (!IsValid() || expected.entry_key.empty() ||
      expected.presentation_revision < 1 ||
      expected.root_web_runtime_id.empty() ||
      expected.application_scope_id.empty()) {
    callback(std::nullopt, {});
    return;
  }
  if (presentation_capture_in_progress_) {
    callback(std::nullopt, {});
    return;
  }
  if (trusted_configured_ && !IsTrustedDocumentActive()) {
    callback(std::nullopt, {});
    return;
  }
  presentation_capture_in_progress_ = true;

  struct CaptureState {
    std::weak_ptr<LifetimeState> lifetime;
    PresentationIdentity expected;
    uint64_t trusted_epoch = 0;
    int attempts_remaining = 30;
    bool completed = false;
    UINT_PTR timer_id = 0;
    std::string token;
  };

  auto state = std::make_shared<CaptureState>();
  state->lifetime = lifetime_state_;
  state->expected = expected;
  state->trusted_epoch = trusted_epoch_;
  state->token = "coreframe-" + std::to_string(
                                  static_cast<uint64_t>(
                                      std::chrono::steady_clock::now()
                                          .time_since_epoch()
                                          .count()));

  auto active = [state]() -> Webview * {
    const std::shared_ptr<LifetimeState> lifetime = state->lifetime.lock();
    if (!lifetime || lifetime->owner == nullptr) return nullptr;
    Webview *owner = lifetime->owner;
    if (!owner->IsValid()) return nullptr;
    if (owner->trusted_configured_ &&
        (!owner->IsTrustedDocumentActive() ||
         owner->trusted_epoch_ != state->trusted_epoch)) {
      return nullptr;
    }
    wil::unique_cotaskmem_string source;
    BOOL visible = FALSE;
    RECT bounds{};
    if (FAILED(owner->webview_->get_Source(&source)) || source == nullptr ||
        FAILED(owner->webview_controller_->get_IsVisible(&visible)) ||
        visible != TRUE ||
        FAILED(owner->webview_controller_->get_Bounds(&bounds)) ||
        bounds.right <= bounds.left || bounds.bottom <= bounds.top) {
      return nullptr;
    }
    return owner;
  };

  auto poll = std::make_shared<std::function<void()>>();
  const std::weak_ptr<std::function<void()>> weak_poll = poll;
  auto finish = [state, callback = std::move(callback)](
                    std::optional<PresentationIdentity> identity,
                    std::vector<uint8_t> png) mutable {
    if (state->completed) return;
    state->completed = true;
    if (const std::shared_ptr<LifetimeState> lifetime = state->lifetime.lock();
        lifetime && lifetime->owner != nullptr) {
      lifetime->owner->presentation_capture_in_progress_ = false;
    }
    CancelPresentationTimer(state->timer_id);
    state->timer_id = 0;
    callback(std::move(identity), std::move(png));
  };

  auto execute = [state, active](
                     const std::string &script,
                     std::function<void(bool, const std::string &)> done) {
    Webview *owner = active();
    if (owner == nullptr || state->completed) {
      done(false, {});
      return;
    }
    owner->ExecuteScript(
        script, [state, done = std::move(done)](bool success,
                                                 const std::string &json) {
          if (state->completed) return;
          done(success, json);
        });
  };

  static constexpr char kIdentityScript[] =
      "(()=>{const d=document.documentElement.dataset;return {"
      "entryKey:d.coreframeEntryKey||'',"
      "presentationRevision:Number(d.coreframePresentationRevision||0),"
      "rootWebRuntimeId:d.coreframeRootWebRuntimeId||'',"
      "applicationScopeId:d.coreframeApplicationScopeId||''};})()";
  static constexpr char kPaintScript[] =
      "document.documentElement.dataset.coreframePaintToken||''";

  auto read_identity = std::make_shared<std::function<void()>>();
  auto capture = std::make_shared<std::function<void()>>();

  *capture = [state, active, execute, finish]() mutable {
    Webview *owner = active();
    if (owner == nullptr) {
      finish(std::nullopt, {});
      return;
    }
    auto stream =
        std::make_shared<wil::com_ptr<IStream>>(CreateCaptureStream());
    if (!*stream) {
      finish(std::nullopt, {});
      return;
    }
    const HRESULT hr = owner->webview_->CapturePreview(
        COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG, stream->get(),
        Microsoft::WRL::Callback<
            ICoreWebView2CapturePreviewCompletedHandler>(
            [state, active, execute, finish,
             stream](HRESULT error_code) mutable -> HRESULT {
              if (state->completed || error_code != S_OK) {
                finish(std::nullopt, {});
                return S_OK;
              }
              std::vector<uint8_t> png = ReadCaptureStream(stream->get());
              if (png.empty() || active() == nullptr) {
                finish(std::nullopt, {});
                return S_OK;
              }
              execute(
                  kIdentityScript,
                  [state, active, finish,
                   png = std::move(png)](bool success,
                                         const std::string &json) mutable {
                    const std::optional<PresentationIdentity> observed =
                        success ? PresentationIdentityFromScriptResult(json)
                                : std::nullopt;
                    if (active() == nullptr || !observed ||
                        !SamePresentationIdentity(*observed, state->expected)) {
                      finish(std::nullopt, {});
                    } else {
                      finish(observed, std::move(png));
                    }
                  });
              return S_OK;
            })
            .Get());
    if (FAILED(hr)) finish(std::nullopt, {});
  };

  *read_identity = [state, active, execute, finish, capture]() {
    execute(
        kIdentityScript,
        [state, active, finish, capture](bool success,
                                        const std::string &json) mutable {
          const std::optional<PresentationIdentity> observed =
              success ? PresentationIdentityFromScriptResult(json)
                      : std::nullopt;
          if (active() == nullptr || !observed ||
              !SamePresentationIdentity(*observed, state->expected)) {
            finish(std::nullopt, {});
            return;
          }
          (*capture)();
        });
  };

  *poll = [state, active, execute, finish, read_identity, weak_poll]() mutable {
    const std::shared_ptr<std::function<void()>> current_poll =
        weak_poll.lock();
    if (!current_poll) {
      finish(std::nullopt, {});
      return;
    }
    execute(
        kPaintScript,
        [state, active, finish, read_identity,
         poll = std::move(current_poll)](bool success,
                                         const std::string &json) mutable {
          if (success && json.find(state->token) != std::string::npos) {
            (*read_identity)();
            return;
          }
          if (state->attempts_remaining <= 0 || active() == nullptr) {
            finish(std::nullopt, {});
            return;
          }
          --state->attempts_remaining;
          state->timer_id = SchedulePresentationTimer(
              [state, poll]() {
                state->timer_id = 0;
                (*poll)();
              },
              16);
          if (state->timer_id == 0) {
            finish(std::nullopt, {});
          }
        });
  };

  const std::string settle_script =
      "(()=>{const element=document.activeElement;const end=element?.selectionEnd;"
      "if(typeof end==='number'&&typeof element?.setSelectionRange==='function')"
      "element.setSelectionRange(end,end);globalThis.getSelection?.()?.removeAllRanges();"
      "if(element instanceof HTMLElement)element.blur();"
      "requestAnimationFrame(()=>requestAnimationFrame("
      "()=>document.documentElement.dataset.coreframePaintToken='" +
      state->token + "'));return true;})()";
  execute(settle_script, [state, finish, poll](bool success,
                                                const std::string &) mutable {
    if (!success && !state->completed) {
      finish(std::nullopt, {});
      return;
    }
    if (state->completed) return;
    state->timer_id = SchedulePresentationTimer(
        [state, poll]() {
          state->timer_id = 0;
          (*poll)();
        },
        16);
    if (state->timer_id == 0) finish(std::nullopt, {});
  });
}

void Webview::InvalidateTrustedDocument() {
  trusted_active_ = false;
  trusted_ready_ = false;
  if (trusted_security_event_callback_) {
    trusted_security_event_callback_("documentInvalidated", "", "",
                                       trusted_epoch_);
  }
}

bool Webview::IsTrustedUrl(const std::string &url) const {
  if (trusted_origin_.empty()) return false;
  const std::string canonical = CanonicalizeNavigationUrl(url);
  if (canonical.rfind(trusted_origin_, 0) != 0 ||
      canonical.size() <= trusted_origin_.size()) {
    return false;
  }
  const char boundary = canonical[trusted_origin_.size()];
  return boundary == '/' || boundary == '?' || boundary == '#';
}

bool Webview::IsTrustedDocumentActive() const {
  if (!trusted_active_ || !trusted_ready_ ||
      trusted_authorized_url_.empty()) {
    return false;
  }
  wil::unique_cotaskmem_string source;
  if (FAILED(webview_->get_Source(&source)) || source == nullptr) return false;
  const std::string current = util::Utf8FromUtf16(source.get());
  return IsTrustedUrl(current) &&
         CanonicalizeNavigationUrl(current) ==
             CanonicalizeNavigationUrl(trusted_authorized_url_);
}

bool Webview::Suspend() {
  if (!IsValid()) {
    return false;
  }

  wil::com_ptr<ICoreWebView2_3> webview;
  webview = webview_.query<ICoreWebView2_3>();
  if (!webview) {
    return false;
  }

  webview_controller_->put_IsVisible(false);
  return webview->TrySuspend(
             Callback<ICoreWebView2TrySuspendCompletedHandler>(
                 [](HRESULT error_code, BOOL is_successful) -> HRESULT {
                   return S_OK;
                 })
                 .Get()) == S_OK;
}

bool Webview::Resume() {
  if (!IsValid()) {
    return false;
  }

  wil::com_ptr<ICoreWebView2_3> webview;
  webview = webview_.query<ICoreWebView2_3>();
  if (!webview) {
    return false;
  }
  return webview->Resume() == S_OK &&
         webview_controller_->put_IsVisible(true) == S_OK;
}

bool Webview::SetVirtualHostNameMapping(
    const std::string &hostName, const std::string &path,
    WebviewHostResourceAccessKind accessKind) {
  if (!IsValid()) {
    return false;
  }

  wil::com_ptr<ICoreWebView2_3> webview;
  webview = webview_.query<ICoreWebView2_3>();
  if (!webview) {
    return false;
  }

  COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND accessKindIntValue =
      COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY;
  switch (accessKind) {
  case WebviewHostResourceAccessKind::Allow:
    accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW;
    break;
  case WebviewHostResourceAccessKind::DenyCors:
    accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS;
    break;
  case WebviewHostResourceAccessKind::Deny:
    accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY;
    break;
  }

  return webview->SetVirtualHostNameToFolderMapping(
      util::Utf16FromUtf8(hostName).c_str(), util::Utf16FromUtf8(path).c_str(),
      accessKindIntValue);
}

bool Webview::ClearVirtualHostNameMapping(const std::string &hostName) {
  if (!IsValid()) {
    return false;
  }

  wil::com_ptr<ICoreWebView2_3> webview;
  webview = webview_.query<ICoreWebView2_3>();
  if (!webview) {
    return false;
  }

  return webview->ClearVirtualHostNameToFolderMapping(
      util::Utf16FromUtf8(hostName).c_str());
}

void Webview::UpdateDownloadProgress(ICoreWebView2DownloadOperation *download) {
  if (!download) {
    return;
  }

  const std::weak_ptr<LifetimeState> weak_state = lifetime_state_;
  EventRegistrationToken bytes_received_token{};
  download->add_BytesReceivedChanged(
      Callback<ICoreWebView2BytesReceivedChangedEventHandler>(
          [weak_state](ICoreWebView2DownloadOperation *download,
                       IUnknown *args) -> HRESULT {
            const std::shared_ptr<LifetimeState> state = weak_state.lock();
            if (state && state->owner &&
                state->owner->download_event_callback_) {
              INT64 recvd = 0;
              download->get_BytesReceived(&recvd);
              INT64 total = 0;
              download->get_TotalBytesToReceive(&total);

              wil::unique_cotaskmem_string uri;
              download->get_Uri(&uri);
              wil::unique_cotaskmem_string resultFilePath;
              download->get_ResultFilePath(&resultFilePath);
              state->owner->download_event_callback_(
                  {WebviewDownloadEventKind::DownloadProgress,
                   util::Utf8FromUtf16(uri.get()),
                   util::Utf8FromUtf16(resultFilePath.get()), recvd, total});
            }
            return S_OK;
          })
          .Get(),
      &bytes_received_token);

  EventRegistrationToken state_changed_token{};
  download->add_StateChanged(
      Callback<ICoreWebView2StateChangedEventHandler>(
          [weak_state](ICoreWebView2DownloadOperation *download,
                       IUnknown *args) -> HRESULT {
            COREWEBVIEW2_DOWNLOAD_STATE state;
            download->get_State(&state);
            switch (state) {
            case COREWEBVIEW2_DOWNLOAD_STATE_IN_PROGRESS:
              break;
            case COREWEBVIEW2_DOWNLOAD_STATE_INTERRUPTED:
              // Here developer can take different actions based on
              // `download->InterruptReason`. For example, show an error
              // message to the end user.
              break;
            case COREWEBVIEW2_DOWNLOAD_STATE_COMPLETED: {
              const std::shared_ptr<LifetimeState> lifetime = weak_state.lock();
              if (lifetime && lifetime->owner &&
                  lifetime->owner->download_event_callback_) {
                wil::unique_cotaskmem_string uri;
                download->get_Uri(&uri);
                wil::unique_cotaskmem_string resultFilePath;
                download->get_ResultFilePath(&resultFilePath);
                INT64 recvd = 0;
                download->get_BytesReceived(&recvd);
                INT64 total = 0;
                download->get_TotalBytesToReceive(&total);
                lifetime->owner->download_event_callback_(
                    {WebviewDownloadEventKind::DownloadCompleted,
                     util::Utf8FromUtf16(uri.get()),
                     util::Utf8FromUtf16(resultFilePath.get()), recvd, total});
              }
              break;
            }
            }
            return S_OK;
          })
          .Get(),
      &state_changed_token);
}

} // namespace coreframe_flutter_windows
