#include "common/method_channel_utils.h"
#include "webview/webview_internal.h"

#include <gio/gio.h>
#include <libsoup/soup.h>
#include <cstring>

namespace {

constexpr const gchar *kBridgeWorld = "coreframe-bridge";
constexpr const gchar *kBridgeHandler = "__coreframe_trusted_bridge";

gchar *loopback_origin(const gchar *value) {
  if (value == nullptr) {
    return nullptr;
  }
  GError *error = nullptr;
  GUri *uri = g_uri_parse(value, G_URI_FLAGS_NONE, &error);
  if (uri == nullptr) {
    g_clear_error(&error);
    return nullptr;
  }
  const gchar *scheme = g_uri_get_scheme(uri);
  const gchar *host = g_uri_get_host(uri);
  const gint port = g_uri_get_port(uri);
  const bool trusted = scheme != nullptr && host != nullptr &&
                       g_ascii_strcasecmp(scheme, "http") == 0 &&
                       g_ascii_strcasecmp(host, "localhost") == 0 &&
                       port >= 1024 && port <= 65535 &&
                       g_uri_get_userinfo(uri) == nullptr;
  gchar *origin = trusted ? g_strdup_printf("http://localhost:%d", port)
                          : nullptr;
  g_uri_unref(uri);
  return origin;
}

bool trusted_uri(LinuxWebView *webview, const gchar *value) {
  if (webview == nullptr || webview->trusted_origin == nullptr) return false;
  gchar *origin = loopback_origin(value);
  const bool trusted = g_strcmp0(origin, webview->trusted_origin) == 0;
  g_free(origin);
  return trusted;
}

bool external_http_uri(const gchar *value) {
  GError *error = nullptr;
  GUri *uri = value == nullptr
                  ? nullptr
                  : g_uri_parse(value, G_URI_FLAGS_NONE, &error);
  if (uri == nullptr) {
    g_clear_error(&error);
    return false;
  }
  const gchar *scheme = g_uri_get_scheme(uri);
  const bool allowed = scheme != nullptr &&
                       (g_ascii_strcasecmp(scheme, "http") == 0 ||
                        g_ascii_strcasecmp(scheme, "https") == 0) &&
                       g_uri_get_host(uri) != nullptr &&
                       *g_uri_get_host(uri) != '\0' &&
                       g_uri_get_userinfo(uri) == nullptr;
  g_uri_unref(uri);
  return allowed;
}

void emit_security_event(LinuxWebView *webview, const gchar *kind,
                         const gchar *url = nullptr,
                         const gchar *detail_name = nullptr,
                         const gchar *detail = nullptr) {
  FlValue *event = make_event("trustedSecurityEvent");
  fl_value_set_string_take(event, "kind", fl_value_new_string(kind));
  fl_value_set_string_take(
      event, "controllerId",
      fl_value_new_string(webview->trusted_controller_id != nullptr
                              ? webview->trusted_controller_id
                              : ""));
  fl_value_set_string_take(
      event, "origin",
      fl_value_new_string(webview->trusted_origin != nullptr
                              ? webview->trusted_origin
                              : ""));
  fl_value_set_string_take(event, "epoch",
                           fl_value_new_int(webview->trusted_epoch));
  fl_value_set_string_take(event, "isMainFrame", fl_value_new_bool(TRUE));
  if (url != nullptr) {
    fl_value_set_string_take(event, "url", fl_value_new_string(url));
  }
  if (detail_name != nullptr && detail != nullptr) {
    fl_value_set_string_take(event, detail_name, fl_value_new_string(detail));
  }
  send_event(webview, event);
}

void invalidate_document(LinuxWebView *webview) {
  webview->trusted_active = FALSE;
  webview->trusted_ready = FALSE;
  if (webview->trusted_dispatch_cancellable != nullptr) {
    g_cancellable_cancel(webview->trusted_dispatch_cancellable);
    g_clear_object(&webview->trusted_dispatch_cancellable);
  }
  emit_security_event(webview, "documentInvalidated");
}

void bridge_message_cb(WebKitUserContentManager *,
                       WebKitJavascriptResult *result, gpointer user_data) {
  LinuxWebView *webview = static_cast<LinuxWebView *>(user_data);
  const gchar *active_uri = webkit_web_view_get_uri(webview->web_view);
  if (!webview->trusted_active || !trusted_uri(webview, active_uri)) return;
  JSCValue *value = webkit_javascript_result_get_js_value(result);
  gchar *message = value == nullptr ? nullptr : jsc_value_to_string(value);
  if (message == nullptr) return;
  emit_security_event(webview, "message", active_uri, "message", message);
  g_free(message);
}

typedef struct {
  gint references;
  LinuxWebView *webview;
} TrustedLifetime;

TrustedLifetime *lifetime_ref(TrustedLifetime *lifetime) {
  g_atomic_int_inc(&lifetime->references);
  return lifetime;
}

void lifetime_unref(TrustedLifetime *lifetime) {
  if (g_atomic_int_dec_and_test(&lifetime->references)) g_free(lifetime);
}

typedef struct {
  TrustedLifetime *lifetime;
  WebKitPolicyDecision *decision;
  gchar *request_id;
  gchar *uri;
  gchar *frame;
  gchar *navigation_kind;
  guint64 epoch;
  gboolean user_initiated;
  gboolean popup;
  gboolean restore_active;
  gboolean restore_ready;
  gchar *restore_uri;
  guint timeout_id;
} PendingTrustedNavigation;

constexpr guint kTrustedNavigationTimeoutSeconds = 5;

void pending_trusted_navigation_free(gpointer data) {
  PendingTrustedNavigation *pending =
      static_cast<PendingTrustedNavigation *>(data);
  if (pending->timeout_id != 0) g_source_remove(pending->timeout_id);
  if (pending->decision != nullptr) {
    webkit_policy_decision_ignore(pending->decision);
    g_object_unref(pending->decision);
  }
  lifetime_unref(pending->lifetime);
  g_free(pending->request_id);
  g_free(pending->uri);
  g_free(pending->frame);
  g_free(pending->navigation_kind);
  g_free(pending->restore_uri);
  g_free(pending);
}

void restore_document_after_block(LinuxWebView *webview,
                                  PendingTrustedNavigation *pending) {
  if (webview == nullptr || pending == nullptr || !pending->restore_active ||
      !pending->restore_ready || pending->restore_uri == nullptr) {
    return;
  }
  const gchar *current_uri = webkit_web_view_get_uri(webview->web_view);
  if (current_uri == nullptr ||
      g_strcmp0(current_uri, pending->restore_uri) != 0) {
    return;
  }
  webview->trusted_active = TRUE;
  webview->trusted_ready = TRUE;
  g_free(webview->trusted_authorized_uri);
  webview->trusted_authorized_uri = g_strdup(pending->restore_uri);
  emit_security_event(webview, "documentRestored", pending->restore_uri);
}

void cancel_trusted_navigations(LinuxWebView *webview) {
  if (webview->trusted_pending_navigations != nullptr) {
    g_hash_table_remove_all(webview->trusted_pending_navigations);
  }
}

gboolean trusted_navigation_timeout_cb(gpointer user_data) {
  PendingTrustedNavigation *pending =
      static_cast<PendingTrustedNavigation *>(user_data);
  pending->timeout_id = 0;
  LinuxWebView *webview = pending->lifetime->webview;
  if (webview == nullptr || webview->trusted_pending_navigations == nullptr) {
    return G_SOURCE_REMOVE;
  }
  restore_document_after_block(webview, pending);
  gchar *key = g_strdup(pending->request_id);
  emit_security_event(webview, "navigationBlocked", pending->uri, "reason",
                      "timeout");
  g_hash_table_remove(webview->trusted_pending_navigations, key);
  g_free(key);
  return G_SOURCE_REMOVE;
}

const gchar *request_frame(WebKitURIRequest *request) {
  // Sec-Fetch-Dest is supplied by WebKit's request, not by page JavaScript.
  // Unknown destinations fail closed and the engine fixture pins this proof.
  SoupMessageHeaders *headers =
      request == nullptr ? nullptr : webkit_uri_request_get_http_headers(request);
  const gchar *destination =
      headers == nullptr
          ? nullptr
          : soup_message_headers_get_one(headers, "Sec-Fetch-Dest");
  if (g_strcmp0(destination, "document") == 0) return "main";
  if (g_strcmp0(destination, "iframe") == 0 ||
      g_strcmp0(destination, "frame") == 0) {
    return "child";
  }
  return nullptr;
}

gboolean pending_main_navigation(LinuxWebView *webview, const gchar *uri) {
  if (webview == nullptr || uri == nullptr ||
      webview->trusted_navigation_kind == nullptr ||
      webview->trusted_navigation_uri == nullptr ||
      webview->trusted_navigation_deadline < g_get_monotonic_time() ||
      g_strcmp0(webview->trusted_navigation_uri, uri) != 0) {
    return FALSE;
  }
  return g_strcmp0(webview->trusted_navigation_kind, "initial") == 0 ||
         g_strcmp0(webview->trusted_navigation_kind, "explicit") == 0 ||
         g_strcmp0(webview->trusted_navigation_kind, "history") == 0 ||
         g_strcmp0(webview->trusted_navigation_kind, "reload") == 0;
}

gchar *consume_navigation_kind(LinuxWebView *webview, const gchar *uri) {
  if (webview->trusted_navigation_kind != nullptr &&
      webview->trusted_navigation_deadline >= g_get_monotonic_time() &&
      (webview->trusted_navigation_uri == nullptr ||
       g_strcmp0(webview->trusted_navigation_uri, uri) == 0)) {
    gchar *kind = g_strdup(webview->trusted_navigation_kind);
    g_clear_pointer(&webview->trusted_navigation_kind, g_free);
    g_clear_pointer(&webview->trusted_navigation_uri, g_free);
    webview->trusted_navigation_deadline = 0;
    return kind;
  }
  g_clear_pointer(&webview->trusted_navigation_kind, g_free);
  g_clear_pointer(&webview->trusted_navigation_uri, g_free);
  webview->trusted_navigation_deadline = 0;
  return g_strdup(webview->trusted_active ? "other" : "redirect");
}

void emit_navigation_request(LinuxWebView *webview,
                             PendingTrustedNavigation *pending) {
  FlValue *event = make_event("trustedSecurityEvent");
  fl_value_set_string_take(event, "kind",
                           fl_value_new_string("navigationRequest"));
  fl_value_set_string_take(event, "requestId",
                           fl_value_new_string(pending->request_id));
  fl_value_set_string_take(event, "controllerId",
                           fl_value_new_string(webview->trusted_controller_id));
  fl_value_set_string_take(
      event, "origin", fl_value_new_string(webview->trusted_origin));
  fl_value_set_string_take(event, "epoch", fl_value_new_int(pending->epoch));
  fl_value_set_string_take(event, "url", fl_value_new_string(pending->uri));
  fl_value_set_string_take(event, "frame", fl_value_new_string(pending->frame));
  fl_value_set_string_take(event, "isMainFrame",
                           fl_value_new_bool(g_strcmp0(pending->frame, "main") == 0));
  fl_value_set_string_take(
      event, "navigationKind",
      fl_value_new_string(pending->navigation_kind));
  fl_value_set_string_take(event, "isUserInitiated",
                           fl_value_new_bool(pending->user_initiated));
  send_event(webview, event);
}

typedef struct {
  TrustedLifetime *lifetime;
  FlMethodCall *method_call;
  GCancellable *cancellable;
  guint timeout_id;
  gboolean responded;
} DispatchContext;

constexpr guint kDispatchTimeoutSeconds = 5;

gboolean dispatch_timeout_cb(gpointer user_data) {
  DispatchContext *context = static_cast<DispatchContext *>(user_data);
  context->timeout_id = 0;
  if (context->responded) return G_SOURCE_REMOVE;
  context->responded = TRUE;
  respond(context->method_call,
          error_response("dispatch_timeout",
                         "The trusted document dispatch timed out."));
  g_cancellable_cancel(context->cancellable);
  return G_SOURCE_REMOVE;
}

void dispatch_finished_cb(GObject *object, GAsyncResult *result,
                          gpointer user_data) {
  DispatchContext *context = static_cast<DispatchContext *>(user_data);
  LinuxWebView *webview = context->lifetime->webview;
  GError *error = nullptr;
  JSCValue *value = webkit_web_view_evaluate_javascript_finish(
      WEBKIT_WEB_VIEW(object), result, &error);
  if (value != nullptr) g_object_unref(value);
  if (context->timeout_id != 0) {
    g_source_remove(context->timeout_id);
    context->timeout_id = 0;
  }
  if (!context->responded) {
    context->responded = TRUE;
    if (error != nullptr) {
      respond(context->method_call,
              error_response("untrusted_document",
                             "The trusted document changed before dispatch."));
    } else {
      respond(context->method_call, success_response());
    }
  }
  g_clear_error(&error);
  if (webview != nullptr &&
      webview->trusted_dispatch_cancellable == context->cancellable) {
    g_clear_object(&webview->trusted_dispatch_cancellable);
  }
  g_clear_object(&context->cancellable);
  g_object_unref(context->method_call);
  lifetime_unref(context->lifetime);
  g_free(context);
}

} // namespace

gboolean configure_trusted_webview(LinuxWebView *webview, FlValue *args,
                                    GError **error) {
  const gchar *initial_location = map_lookup_string(args, "initialLocation");
  const gboolean diagnostics_enabled =
      map_lookup_bool(args, "diagnosticsEnabled", FALSE);
  gchar *origin = loopback_origin(initial_location);
  if (webview->trusted_configured) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS,
                "The WebView is already configured.");
    g_free(origin);
    return FALSE;
  }
  if (webkit_get_major_version() < 2 ||
      (webkit_get_major_version() == 2 && webkit_get_minor_version() < 42)) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                "Coreframe WebViews require WebKitGTK 2.42 or newer.");
    g_free(origin);
    return FALSE;
  }
  if (origin == nullptr) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                "Invalid loopback bootstrap request.");
    g_free(origin);
    return FALSE;
  }
  webview->trusted_configured = TRUE;
  TrustedLifetime *lifetime = g_new0(TrustedLifetime, 1);
  lifetime->references = 1;
  lifetime->webview = webview;
  webview->trusted_lifetime = lifetime;
  webview->trusted_controller_id = g_uuid_string_random();
  webview->trusted_origin = origin;
  webview->trusted_initial_uri = g_strdup(initial_location);
  webview->trusted_pending_navigations = g_hash_table_new_full(
      g_str_hash, g_str_equal, g_free, pending_trusted_navigation_free);

  if (webview->trusted_initial_uri == nullptr ||
      !trusted_uri(webview, webview->trusted_initial_uri) ||
      !webkit_user_content_manager_register_script_message_handler_in_world(
          webview->content_manager, kBridgeHandler, kBridgeWorld)) {
    webview->trusted_configured = FALSE;
    destroy_trusted_webview_state(webview);
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED,
                "The trusted bridge could not be installed.");
    return FALSE;
  }
  gchar *signal = g_strdup_printf("script-message-received::%s", kBridgeHandler);
  g_signal_connect(webview->content_manager, signal,
                   G_CALLBACK(bridge_message_cb), webview);
  g_free(signal);

  // WebKit user-content URL patterns reject explicit ports. Use the narrowest
  // valid loopback pattern and preserve the exact-port boundary inside each
  // script as well as in the native bridge and navigation checks.
  const gchar *script_allow_list[] = {"http://localhost/*", nullptr};
  const gchar *console_source = 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{sendEnvelope({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{}}}"
      : "";
  gchar *page_source = g_strdup_printf(
      "(()=>{if(location.origin!=='%s')return;"
      "const sendEnvelope=value=>document.dispatchEvent(new "
      "CustomEvent('__coreframe_bridge',{detail:JSON.stringify(value)}));"
      "const send=(channelName,message)=>sendEnvelope({coreframeType:'channel',channelName,message});"
      "globalThis.CoreframeFlutter={postMessage:message=>send('CoreframeFlutter',String(message))};"
      "let active=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===active)return;active=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);%s})();",
      webview->trusted_origin, console_source);
  WebKitUserScript *page_script = webkit_user_script_new(
      page_source, WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
      WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, script_allow_list, nullptr);
  webkit_user_content_manager_add_script(webview->content_manager, page_script);
  webkit_user_script_unref(page_script);
  g_free(page_source);

  gchar *isolated_source = g_strdup_printf(
      "(()=>{if(location.origin!=='%s')return;"
      "document.addEventListener('__coreframe_bridge',function(event){"
      "window.webkit.messageHandlers.__coreframe_trusted_bridge.postMessage(String(event.detail));});})()",
      webview->trusted_origin);
  WebKitUserScript *isolated_script = webkit_user_script_new_for_world(
      isolated_source,
      WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
      WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, kBridgeWorld,
      script_allow_list, nullptr);
  webkit_user_content_manager_add_script(webview->content_manager,
                                          isolated_script);
  webkit_user_script_unref(isolated_script);
  g_free(isolated_source);
  return TRUE;
}

FlValue *trusted_configuration(LinuxWebView *webview) {
  FlValue *configuration = fl_value_new_map();
  fl_value_set_string_take(configuration, "origin",
                           fl_value_new_string(webview->trusted_origin));
  fl_value_set_string_take(
      configuration, "controllerId",
      fl_value_new_string(webview->trusted_controller_id));
  return configuration;
}

gboolean start_trusted_webview(LinuxWebView *webview) {
  if (!webview->trusted_configured || webview->trusted_initial_started ||
      webview->trusted_initial_uri == nullptr) {
    return FALSE;
  }
  webview->trusted_initial_started = TRUE;
  g_free(webview->trusted_navigation_kind);
  g_free(webview->trusted_navigation_uri);
  webview->trusted_navigation_kind = g_strdup("initial");
  webview->trusted_navigation_uri = g_strdup(webview->trusted_initial_uri);
  webview->trusted_navigation_deadline =
      g_get_monotonic_time() + 5 * G_TIME_SPAN_SECOND;
  webkit_web_view_load_uri(webview->web_view, webview->trusted_initial_uri);
  return TRUE;
}

gboolean trusted_decide_policy(LinuxWebView *webview,
                                WebKitPolicyDecision *decision,
                                WebKitPolicyDecisionType type) {
  if (!webview->trusted_configured) {
    return FALSE;
  }
  if (type == WEBKIT_POLICY_DECISION_TYPE_RESPONSE) {
    WebKitResponsePolicyDecision *response =
        WEBKIT_RESPONSE_POLICY_DECISION(decision);
    WebKitURIRequest *request =
        webkit_response_policy_decision_get_request(response);
    const gchar *uri =
        request == nullptr ? nullptr : webkit_uri_request_get_uri(request);
    const gboolean main_resource =
        webkit_response_policy_decision_is_main_frame_main_resource(response);
    const gboolean authorized_main_resource =
        main_resource && uri != nullptr &&
        webview->trusted_authorized_uri != nullptr &&
        trusted_uri(webview, uri) &&
        g_strcmp0(uri, webview->trusted_authorized_uri) == 0;
    if (main_resource && !authorized_main_resource) {
      webkit_policy_decision_ignore(decision);
      emit_security_event(webview, "navigationBlocked", uri, "reason",
                          "missing-frame-evidence");
    } else {
      webkit_policy_decision_use(decision);
    }
    return TRUE;
  }
  if (type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION &&
      type != WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
    return FALSE;
  }
  WebKitNavigationPolicyDecision *navigation =
      WEBKIT_NAVIGATION_POLICY_DECISION(decision);
  WebKitNavigationAction *action =
      webkit_navigation_policy_decision_get_navigation_action(navigation);
  WebKitURIRequest *request = webkit_navigation_action_get_request(action);
  const gchar *uri = request == nullptr ? nullptr
                                        : webkit_uri_request_get_uri(request);
  const gboolean user_gesture =
      action != nullptr && webkit_navigation_action_is_user_gesture(action);
  const gchar *frame = type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION
                           ? "main"
                           : request_frame(request);
  const gchar *frame_name = action == nullptr
                                ? nullptr
                                : webkit_navigation_action_get_frame_name(
                                      action);
  if (frame_name == nullptr) {
    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    frame_name = webkit_navigation_policy_decision_get_frame_name(navigation);
    G_GNUC_END_IGNORE_DEPRECATIONS
  }
  // A host-issued exact-URI token identifies an initial or explicit
  // main-frame navigation when WebKit omits Sec-Fetch-Dest.
  if (frame == nullptr && pending_main_navigation(webview, uri)) {
    frame = "main";
  }
  // Named child frames expose their target frame through the navigation
  // action, so use that native evidence for child classification.
  if (frame == nullptr && frame_name != nullptr && *frame_name != '\0') {
    frame = "child";
  }
  if (uri == nullptr || webview->trusted_pending_navigations == nullptr) {
    webkit_policy_decision_ignore(decision);
    emit_security_event(webview, "navigationBlocked", uri, "reason",
                        "missing-frame-evidence");
    return TRUE;
  }
  if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION &&
      !user_gesture) {
    // Script-created popups can never receive an allowed disposition. Resolve
    // them synchronously so window.open() is not suspended on a Dart round trip.
    webkit_policy_decision_ignore(decision);
    emit_security_event(webview, "navigationBlocked", uri, "reason",
                        "policy");
    return TRUE;
  }
  if (frame == nullptr) {
    if (!webview->trusted_active || !trusted_uri(webview, uri)) {
      webkit_policy_decision_ignore(decision);
      emit_security_event(webview, "navigationBlocked", uri, "reason",
                          "missing-frame-evidence");
      return TRUE;
    }
    // WebKitGTK does not expose a target-frame object from the UI process.
    // Allow trusted ambiguous navigations to reach the response policy, which
    // can distinguish a child document from the main frame's main resource.
    webkit_policy_decision_use(decision);
    return TRUE;
  }
  gchar *navigation_kind = nullptr;
  gboolean restore_active = FALSE;
  gboolean restore_ready = FALSE;
  gchar *restore_uri = nullptr;
  if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
    navigation_kind = g_strdup("popup");
  } else if (g_strcmp0(frame, "main") == 0) {
    restore_active = webview->trusted_active;
    restore_ready = webview->trusted_ready;
    restore_uri = g_strdup(webview->trusted_authorized_uri);
    navigation_kind = consume_navigation_kind(webview, uri);
    cancel_trusted_navigations(webview);
    ++webview->trusted_epoch;
    invalidate_document(webview);
    g_clear_pointer(&webview->trusted_authorized_uri, g_free);
  } else {
    navigation_kind = g_strdup("other");
    if (!webview->trusted_active) {
      g_free(navigation_kind);
      webkit_policy_decision_ignore(decision);
      emit_security_event(webview, "navigationBlocked", uri, "reason",
                          "inactive-main-document");
      return TRUE;
    }
  }

  PendingTrustedNavigation *pending =
      g_new0(PendingTrustedNavigation, 1);
  pending->lifetime = lifetime_ref(
      static_cast<TrustedLifetime *>(webview->trusted_lifetime));
  pending->decision = WEBKIT_POLICY_DECISION(g_object_ref(decision));
  pending->request_id =
      g_strdup_printf("linux-%" G_GUINT64_FORMAT "-%d",
                      webview->trusted_epoch, ++webview->next_request_id);
  pending->uri = g_strdup(uri);
  pending->frame = g_strdup(frame);
  pending->navigation_kind = navigation_kind;
  pending->epoch = webview->trusted_epoch;
  pending->user_initiated = user_gesture;
  pending->popup = type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION;
  pending->restore_active = restore_active;
  pending->restore_ready = restore_ready;
  pending->restore_uri = restore_uri;
  pending->timeout_id = g_timeout_add_seconds(
      kTrustedNavigationTimeoutSeconds, trusted_navigation_timeout_cb,
      pending);
  if (pending->timeout_id == 0) {
    pending_trusted_navigation_free(pending);
    return TRUE;
  }
  g_hash_table_insert(webview->trusted_pending_navigations,
                      g_strdup(pending->request_id), pending);
  emit_navigation_request(webview, pending);
  return TRUE;
}

void trusted_load_changed(LinuxWebView *webview, WebKitLoadEvent load_event) {
  if (!webview->trusted_configured) return;
  const gchar *uri = webkit_web_view_get_uri(webview->web_view);
  if (load_event == WEBKIT_LOAD_COMMITTED &&
      webview->trusted_authorized_uri != nullptr &&
      g_strcmp0(uri, webview->trusted_authorized_uri) == 0 &&
      trusted_uri(webview, uri)) {
    webview->trusted_active = TRUE;
    emit_security_event(webview, "documentActivated", uri);
  }
  if (load_event == WEBKIT_LOAD_FINISHED && webview->trusted_active &&
      webview->trusted_authorized_uri != nullptr &&
      g_strcmp0(uri, webview->trusted_authorized_uri) == 0 &&
      trusted_uri(webview, uri)) {
    emit_security_event(webview, "documentFinished", uri);
  }
}

gboolean trusted_load_uri(LinuxWebView *webview, const gchar *uri,
                           const gchar *navigation_kind) {
  if (!webview->trusted_configured || !webview->trusted_ready) return FALSE;
  if (uri == nullptr || !trusted_uri(webview, uri) ||
      navigation_kind == nullptr) {
    return FALSE;
  }
  g_free(webview->trusted_navigation_kind);
  g_free(webview->trusted_navigation_uri);
  webview->trusted_navigation_kind = g_strdup(navigation_kind);
  webview->trusted_navigation_uri = g_strdup(uri);
  webview->trusted_navigation_deadline =
      g_get_monotonic_time() + 5 * G_TIME_SPAN_SECOND;
  webkit_web_view_load_uri(webview->web_view, uri);
  return TRUE;
}

gboolean complete_trusted_navigation(LinuxWebView *webview, FlValue *args) {
  const gchar *request_id = map_lookup_string(args, "requestId");
  const gchar *controller = map_lookup_string(args, "controllerId");
  const gchar *disposition = map_lookup_string(args, "disposition");
  const gint64 epoch = map_lookup_int(args, "epoch", -1);
  if (!webview->trusted_configured || request_id == nullptr || controller == nullptr ||
      disposition == nullptr || epoch < 1 ||
      g_strcmp0(controller, webview->trusted_controller_id) != 0 ||
      webview->trusted_pending_navigations == nullptr) {
    return FALSE;
  }
  gpointer stored_key = nullptr;
  gpointer stored_value = nullptr;
  g_hash_table_lookup_extended(webview->trusted_pending_navigations,
                               request_id, &stored_key, &stored_value);
  PendingTrustedNavigation *pending =
      static_cast<PendingTrustedNavigation *>(stored_value);
  if (pending == nullptr || pending->epoch != static_cast<guint64>(epoch) ||
      pending->epoch != webview->trusted_epoch) {
    return FALSE;
  }
  g_hash_table_steal(webview->trusted_pending_navigations, request_id);
  g_free(stored_key);
  if (pending->timeout_id != 0) {
    g_source_remove(pending->timeout_id);
    pending->timeout_id = 0;
  }

  const bool main_frame = g_strcmp0(pending->frame, "main") == 0;
  const bool allow = g_strcmp0(disposition, "allow") == 0 &&
                     !pending->popup &&
                     trusted_uri(webview, pending->uri);
  const bool external = g_strcmp0(disposition, "external") == 0 &&
                        main_frame && pending->user_initiated &&
                        external_http_uri(pending->uri);
  const bool block = g_strcmp0(disposition, "block") == 0;
  if (!allow && !external && !block) {
    pending_trusted_navigation_free(pending);
    return FALSE;
  }
  if (allow) {
    if (main_frame) {
      g_free(webview->trusted_authorized_uri);
      webview->trusted_authorized_uri = g_strdup(pending->uri);
    }
    webkit_policy_decision_use(pending->decision);
  } else {
    webkit_policy_decision_ignore(pending->decision);
    restore_document_after_block(webview, pending);
    if (external) {
      g_app_info_launch_default_for_uri(pending->uri, nullptr, nullptr);
    }
    emit_security_event(webview, "navigationBlocked", pending->uri, "reason",
                        external ? "external" : "policy");
  }
  g_clear_object(&pending->decision);
  pending_trusted_navigation_free(pending);
  return TRUE;
}

gboolean mark_trusted_ready(LinuxWebView *webview, FlValue *args) {
  const gchar *controller = map_lookup_string(args, "controllerId");
  const gchar *origin = map_lookup_string(args, "origin");
  const gint64 epoch = map_lookup_int(args, "epoch", -1);
  if (!webview->trusted_active || controller == nullptr || origin == nullptr ||
      g_strcmp0(controller, webview->trusted_controller_id) != 0 ||
      g_strcmp0(origin, webview->trusted_origin) != 0 ||
      epoch != static_cast<gint64>(webview->trusted_epoch)) {
    return FALSE;
  }
  webview->trusted_ready = TRUE;
  return TRUE;
}

void trusted_dispatch(LinuxWebView *webview, FlMethodCall *method_call,
                       FlValue *args) {
  const gchar *controller = map_lookup_string(args, "controllerId");
  const gchar *origin = map_lookup_string(args, "origin");
  const gint64 epoch = map_lookup_int(args, "epoch", -1);
  const gchar *event_name = map_lookup_string(args, "eventName");
  const gchar *message = map_lookup_string(args, "message");
  if (!webview->trusted_active || controller == nullptr || origin == nullptr ||
      event_name == nullptr || message == nullptr ||
      g_strcmp0(controller, webview->trusted_controller_id) != 0 ||
      g_strcmp0(origin, webview->trusted_origin) != 0 ||
      epoch != static_cast<gint64>(webview->trusted_epoch)) {
    respond(method_call,
            error_response("untrusted_document",
                           "No matching trusted document is active."));
    return;
  }
  if (webview->trusted_dispatch_cancellable != nullptr) {
    respond(method_call,
            error_response("duplicate_dispatch",
                           "A trusted document dispatch is already pending."));
    return;
  }
  gchar *event_literal = g_strescape(event_name, nullptr);
  gchar *message_literal = g_strescape(message, nullptr);
  gchar *script = g_strdup_printf(
      "(()=>{if(globalThis.top!==globalThis||location.origin!==\"%s\")"
      "throw new Error('Untrusted document');"
      "window.dispatchEvent(new CustomEvent(\"%s\",{detail:JSON.parse(\"%s\")}));"
      "return true;})()",
      webview->trusted_origin, event_literal, message_literal);
  g_free(event_literal);
  g_free(message_literal);
  DispatchContext *context = g_new0(DispatchContext, 1);
  context->lifetime = lifetime_ref(
      static_cast<TrustedLifetime *>(webview->trusted_lifetime));
  context->method_call = FL_METHOD_CALL(g_object_ref(method_call));
  context->cancellable = g_cancellable_new();
  webview->trusted_dispatch_cancellable =
      G_CANCELLABLE(g_object_ref(context->cancellable));
  context->timeout_id = g_timeout_add_seconds(
      kDispatchTimeoutSeconds, dispatch_timeout_cb, context);
  webkit_web_view_evaluate_javascript(
      webview->web_view, script, -1, nullptr, nullptr,
      context->cancellable, dispatch_finished_cb, context);
  g_free(script);
}

void destroy_trusted_webview_state(LinuxWebView *webview) {
  if (webview->trusted_dispatch_cancellable != nullptr) {
    g_cancellable_cancel(webview->trusted_dispatch_cancellable);
  }
  g_clear_object(&webview->trusted_dispatch_cancellable);
  g_clear_pointer(&webview->trusted_pending_navigations,
                  g_hash_table_unref);
  TrustedLifetime *lifetime =
      static_cast<TrustedLifetime *>(webview->trusted_lifetime);
  if (lifetime != nullptr) {
    lifetime->webview = nullptr;
    lifetime_unref(lifetime);
    webview->trusted_lifetime = nullptr;
  }
  g_clear_pointer(&webview->trusted_controller_id, g_free);
  g_clear_pointer(&webview->trusted_origin, g_free);
  g_clear_pointer(&webview->trusted_initial_uri, g_free);
  g_clear_pointer(&webview->trusted_authorized_uri, g_free);
  g_clear_pointer(&webview->trusted_navigation_kind, g_free);
  g_clear_pointer(&webview->trusted_navigation_uri, g_free);
}
