// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/contextual_tasks/contextual_tasks_ui_service.h"

#include <algorithm>
#include <optional>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/no_destructor.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/companion/text_finder/text_finder_manager.h"
#include "chrome/browser/companion/text_finder/text_highlighter_manager.h"
#include "chrome/browser/contextual_search/contextual_search_service_factory.h"
#include "chrome/browser/contextual_search/contextual_search_web_contents_helper.h"
#include "chrome/browser/contextual_tasks/active_task_context_provider.h"
#include "chrome/browser/contextual_tasks/contextual_search_session_finder.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_cookie_synchronizer.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_eligibility_manager.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_panel_controller.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_service_factory.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_ui.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_ui_interface.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_ui_service_delegate.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_utils.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_window_tracker.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_window_tracker_manager.h"
#include "chrome/browser/contextual_tasks/entry_point_eligibility_manager.h"
#include "chrome/browser/contextual_tasks/guest_opener_user_data.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/sessions/session_tab_helper_factory.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/navigator/browser_navigator.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/webui/webui_embedding_context.h"
#include "chrome/common/webui_url_constants.h"
#include "components/contextual_search/contextual_search_metrics_recorder.h"
#include "components/contextual_search/contextual_search_service.h"
#include "components/contextual_search/contextual_search_session_handle.h"
#include "components/contextual_search/contextual_search_types.h"
#include "components/contextual_tasks/public/account_utils.h"
#include "components/contextual_tasks/public/contextual_task.h"
#include "components/contextual_tasks/public/contextual_tasks_service.h"
#include "components/contextual_tasks/public/features.h"
#include "components/contextual_tasks/public/utils.h"
#include "components/lens/lens_features.h"
#include "components/lens/lens_url_utils.h"
#include "components/omnibox/browser/aim_eligibility_service.h"
#include "components/omnibox/common/logger.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/search_engines/template_url_service.h"
#include "components/search_engines/util.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/shared_highlighting/core/common/fragment_directives_utils.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_fetcher.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/strings/grit/components_strings.h"
#include "components/tabs/public/tab_interface.h"
#include "components/url_formatter/url_formatter.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/schemeful_site.h"
#include "net/base/url_util.h"
#include "pdf/buildflags.h"
#include "third_party/omnibox_proto/chrome_aim_entry_point.pb.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "url/origin.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/lens/lens_media_link_handler.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#endif

#if BUILDFLAG(ENABLE_PDF)
#include "chrome/browser/pdf/pdf_extension_util.h"
#include "components/pdf/browser/pdf_document_helper.h"
#endif  // BUILDFLAG(ENABLE_PDF)

using sessions::SessionTabHelper;

namespace contextual_tasks {

namespace {

// An allowlist of origins to allow account linking features to work with
// Contextual Tasks. This list should only contain trusted domains, defined as:
// must be a google first-party domain, strictly require postMessaging without a
// safer workaround, and have been approved by Chrome security.
// TODO(crbug.com/500717050): Consider moving these to GAIA URL constants or a
// feature param.
constexpr const char* const kAllowedHostsForGuestMessage[] = {
    "myaccount.google.com",
    "oauth-redirect.googleusercontent.com",
    "payments.google.com",
};

bool IsAllowedOriginForGuestMessage(const url::Origin& origin) {
  if (origin.opaque()) {
    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
            "allow-opaque-origin-for-contextual-tasks-testing")) {
      return true;
    }
    return false;
  }
  if (origin.scheme() != url::kHttpsScheme) {
    return false;
  }
  const std::string& host = origin.host();

  // Allow explicit trusted domains.
  for (const char* allowed_host : kAllowedHostsForGuestMessage) {
    if (host == allowed_host) {
      return true;
    }
  }

  // Allow GAIA host from GaiaUrls.
  if (host == GaiaUrls::GetInstance()->gaia_origin().host()) {
    return true;
  }

  // Allow internal corp domains for development.
  if (origin.GetURL().DomainIs("corp.google.com")) {
    return true;
  }

  return false;
}

constexpr net::BackoffEntry::Policy
    kIgnoreFirstErrorRequestAccessTokenBackoffPolicy = {
        // Number of initial errors (in sequence) to ignore before applying
        // exponential back-off rules.
        2,

        // Initial delay for exponential back-off in ms.
        150,

        // Factor by which the waiting time will be multiplied.
        2,

        // Fuzzing percentage. ex: 10% will spread requests randomly
        // between 90%-100% of the calculated time.
        0.2,  // 20%

        // Maximum amount of time we are willing to delay our request in ms.
        2000,  // 2 seconds.

        // Time to keep an entry from being discarded even when it
        // has no significant state, -1 to never discard.
        -1,

        // Don't use initial delay unless the last request was an error.
        false,
};

// Heuristic threshold under which an OAuth token fetch is classified as a
// local cache hit.
constexpr base::TimeDelta kOAuthCacheHitThreshold = base::Milliseconds(30);

constexpr char kAiPageHost[] = "https://google.com";

// Parameters that the search results page must contain at least one of to be
// considered a valid search results page.
constexpr char kSearchQueryKey[] = "q";
constexpr char kLensModeKey[] = "lns_mode";

bool IsSignInDomain(const GURL& url) {
  if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) {
    return false;
  }
  std::vector<std::string> sign_in_domains = GetContextualTasksSignInDomains();
  for (const auto& sign_in_domain : sign_in_domains) {
    if (url.host() == sign_in_domain &&
        !base::EndsWith(url.path(), "Logout",
                        base::CompareCase::INSENSITIVE_ASCII)) {
      return true;
    }
  }
  return false;
}

// LINT.IfChange(EntrypointSource)

enum class EntrypointSource {
  kFromWeb = 0,
  kFromOmnibox = 1,
  kFromNewTabPage = 2,
  kFromLens = 3,

  kMaxValue = kFromLens,
};

// LINT.ThenChange(//tools/metrics/histograms/metadata/contextual_tasks/enums.xml:EntrypointSource)

EntrypointSource ConvertContextualSearchSourceToEntrypointSource(
    contextual_search::ContextualSearchSource source) {
  switch (source) {
    case contextual_search::ContextualSearchSource::kOmnibox:
    case contextual_search::ContextualSearchSource::kOmniboxEverywhere:
      return EntrypointSource::kFromOmnibox;
    case contextual_search::ContextualSearchSource::kNewTabPage:
      return EntrypointSource::kFromNewTabPage;
    case contextual_search::ContextualSearchSource::kLens:
      return EntrypointSource::kFromLens;
    case contextual_search::ContextualSearchSource::kContextualTasks:
      // This shouldn't happen but is it does - just fall through to say it's
      // from web.
    case contextual_search::ContextualSearchSource::kUnknown:
      return EntrypointSource::kFromWeb;
  }
}

#if !BUILDFLAG(IS_ANDROID)
bool ShouldReloadZeroState(const GURL& url, ContextualTasksUiService* service) {
  return base::FeatureList::IsEnabled(
             omnibox::kWebUIOmniboxAskGAboutThisPage) &&
         ContextualTasksUI::IsZeroState(url, service);
}
#else
bool ShouldReloadZeroState(const GURL& url, ContextualTasksUiService* service) {
  return false;
}
#endif

void LoadUrlInSidePanel(content::WebContents* web_contents, const GURL& url) {
  web_contents->GetController().LoadURL(url, content::Referrer(),
                                        ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
                                        std::string());
}

// Returns true if `web_contents` corresponds to an active tab that is attached
// as context in the current contextual search session.
bool IsActiveTabInContext(content::WebContents* web_contents) {
  if (!contextual_tasks::IsContextualTasksUIEnabled() || !web_contents) {
    return false;
  }

  SessionID current_tab_id = SessionTabHelper::IdForTab(web_contents);
  if (!current_tab_id.is_valid()) {
    return false;
  }

  auto* helper =
      ContextualSearchWebContentsHelper::FromWebContents(web_contents);
  if (!helper || !helper->session_handle()) {
    return false;
  }

  auto* session_handle = helper->session_handle();
  for (const auto& file : session_handle->GetSubmittedContextFileInfos()) {
    if (file.tab_session_id.has_value() &&
        file.tab_session_id.value() == current_tab_id) {
      return true;
    }
  }
  for (const auto& file : session_handle->GetUploadedContextFileInfos()) {
    if (file.tab_session_id.has_value() &&
        file.tab_session_id.value() == current_tab_id) {
      return true;
    }
  }
  return false;
}

}  // namespace

ContextualTasksUiService::ContextualTasksUiService(
    Profile* profile,
    std::unique_ptr<ContextualTasksUiServiceDelegate> delegate,
    ContextualTasksService* contextual_tasks_service,
    signin::IdentityManager* identity_manager,
    AimEligibilityService* aim_eligibility_service,
    std::unique_ptr<ContextualTasksEligibilityManager> eligibility_manager,
    std::unique_ptr<ContextualTasksCookieSynchronizer> cookie_synchronizer)
    : profile_(profile),
      delegate_(std::move(delegate)),
      contextual_tasks_service_(contextual_tasks_service),
      identity_manager_(identity_manager),
      aim_eligibility_service_(aim_eligibility_service),
      request_access_token_backoff_(
          &kIgnoreFirstErrorRequestAccessTokenBackoffPolicy),
      cookie_synchronizer_(std::move(cookie_synchronizer)),
      eligibility_manager_(std::move(eligibility_manager)),
      tracker_manager_(
          std::make_unique<ContextualTasksWindowTrackerManager>(profile)) {
  if (eligibility_manager_ && contextual_tasks::ShouldEnableCookiePrefetch()) {
    eligibility_subscription_ =
        eligibility_manager_->RegisterEligibilityChangedCallback(
            base::BindRepeating(&ContextualTasksUiService::OnEligibilityChanged,
                                base::Unretained(this)));
    OnEligibilityChanged(eligibility_manager_->IsEligible());
  }
}

ContextualTasksUiService::~ContextualTasksUiService() = default;

void ContextualTasksUiService::EnsureCookiesSynced(base::OnceClosure callback) {
  if (cookie_synchronizer_) {
    cookie_synchronizer_->CopyCookiesToWebviewStoragePartition(
        std::move(callback));
  } else {
    std::move(callback).Run();
  }
}

void ContextualTasksUiService::Shutdown() {
  for (auto& observer : observers_) {
    observer.OnContextualTasksUiServiceShutdown(this);
  }
  tracker_manager_.reset();
  weak_ptr_factory_.InvalidateWeakPtrs();
  if (current_oauth_fetch_trigger_.has_value()) {
    RecordOAuthMetrics(GoogleServiceAuthError::CreateRequestCanceled(),
                       signin::AccessTokenInfo());
  }
  access_token_fetcher_.reset();
  token_refresh_timer_.Stop();
}

void ContextualTasksUiService::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void ContextualTasksUiService::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void ContextualTasksUiService::OnNavigationToAiPageIntercepted(
    const GURL& url,
    base::WeakPtr<tabs::TabInterface> source_tab,
    bool is_to_new_tab) {
  OMNIBOX_LOG("nav_trace") << "ContextualTasks navigation trace: "
                              "OnNavigationToAiPageIntercepted called for URL: "
                           << url;
  CHECK(contextual_tasks_service_);

  // Starts the access token fetch now so it happens in parallel to the WebUI
  // initializing.
  StartAccessTokenFetch(OAuthFetchTrigger::kAimNavigationInterception);

  // Get the session handle from the source web contents, if provided, to
  // propagate context from the source WebUI.
  std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
      session_handle;
  std::unique_ptr<contextual_search::InputStateModel> input_state_model;
  std::vector<int32_t> selected_tab_ids;
  contextual_search::ContextualSearchSource source =
      contextual_search::ContextualSearchSource::kUnknown;
  if (source_tab) {
    auto* helper = ContextualSearchWebContentsHelper::FromWebContents(
        source_tab->GetContents());
    if (helper) {
      if (helper->session_handle()) {
        session_handle = helper->TakeSessionHandle();
        source = session_handle->GetMetricsRecorder()->source();
      }
      input_state_model = helper->TakeInputStateModel();
      selected_tab_ids = helper->GetSelectedTabIds();
    }
  }
  base::UmaHistogramEnumeration(
      "ContextualTasks.AiPage.NavigationInterceptionSource",
      ConvertContextualSearchSourceToEntrypointSource(source));

  // Create a task for the URL that was just intercepted.
  ContextualTask task = contextual_tasks_service_->CreateTaskFromUrl(url);

  // Configure AskG specific entry point if enabled.
  if (base::FeatureList::IsEnabled(omnibox::kWebUIOmniboxAskGAboutThisPage)) {
    bool has_attached_tab = false;
    if (session_handle) {
      auto submitted_files = session_handle->GetSubmittedContextFileInfos();
      for (const auto& file : submitted_files) {
        if (file.tab_session_id.has_value()) {
          has_attached_tab = true;
          break;
        }
      }
      if (!has_attached_tab) {
        auto uploaded_files = session_handle->GetUploadedContextFileInfos();
        for (const auto& file : uploaded_files) {
          if (file.tab_session_id.has_value()) {
            has_attached_tab = true;
            break;
          }
        }
      }
    }

    if (has_attached_tab &&
        (source == contextual_search::ContextualSearchSource::kOmnibox ||
         source ==
             contextual_search::ContextualSearchSource::kOmniboxEverywhere)) {
      SetInitialEntryPointForTask(
          task.GetTaskId(), omnibox::ChromeAimEntryPoint::
                                DESKTOP_CHROME_COBROWSE_OMNIBOX_TAB_SEARCH);
    }
  }

  // Map the task ID to the intercepted url. This is done so the UI knows which
  // URL to load initially in the embedded frame.
  task_id_to_creation_url_[task.GetTaskId()] = url;

  GURL ui_url = GetContextualTaskUrlForTask(task.GetTaskId());
  // If the CS param is in the URL, add it to the webui, so the
  // chrome_content_browser_client.cc code can properly setup the renderer dark
  // mode preference. This prevents UI flicker.
  std::optional<bool> is_dark_mode = contextual_tasks::GetDarkModeFromUrl(url);
  if (is_dark_mode.has_value()) {
    ui_url = net::AppendOrReplaceQueryParameter(
        ui_url, "cs", is_dark_mode.value() ? "1" : "0");
  }

  content::WebContents* contextual_task_web_contents = nullptr;
  // If the current tab is included in the context list, this navigation should
  // open in the side panel.
  // TODO(crbug.com/462773224): Add test that navigation with current tab in
  // context leads to side panel.
  if (session_handle && source_tab &&
      session_handle->IsTabInContext(
          SessionTabHelper::IdForTab(source_tab->GetContents()))) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: "
           "OnNavigationToAiPageIntercepted opening in side panel";
    AssociateWebContentsToTask(source_tab->GetContents(), task.GetTaskId());
    BrowserWindowInterface* window =
        webui::GetBrowserWindowInterface(source_tab->GetContents());
    if (window) {
      auto* controller = ContextualTasksPanelController::From(window);
      controller->Show();
      contextual_task_web_contents = controller->GetActiveWebContents();
    }
  } else {
    base::RecordAction(
        base::UserMetricsAction("ContextualTasks.AimFullTab.Shown"));
    if (!is_to_new_tab && source_tab) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: "
             "OnNavigationToAiPageIntercepted loading in source tab";
      source_tab->GetContents()->GetController().LoadURLWithParams(
          content::NavigationController::LoadURLParams(ui_url));
      contextual_task_web_contents = source_tab->GetContents();
    } else {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: "
             "OnNavigationToAiPageIntercepted opening in new tab";
      NavigateParams params(profile_, ui_url,
                            ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
      params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;

      Navigate(&params);
      contextual_task_web_contents = params.navigated_or_inserted_contents;
    }
  }
  // Associate the web contents with the task and set the session handle if
  // provided.
  if (contextual_task_web_contents) {
    if (session_handle) {
      auto* helper =
          ContextualSearchWebContentsHelper::GetOrCreateForWebContents(
              contextual_task_web_contents);
      helper->SetTaskSession(task.GetTaskId(), std::move(session_handle),
                             std::move(input_state_model),
                             std::move(selected_tab_ids));
    }
    AssociateWebContentsToTask(contextual_task_web_contents, task.GetTaskId());
  }
}

void ContextualTasksUiService::OnOAuthTokenReceived(
    GoogleServiceAuthError error,
    signin::AccessTokenInfo access_token_info) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: OnOAuthTokenReceived called "
         "with error state: "
      << static_cast<int>(error.state());
  // Clear the fetcher as it's done.
  access_token_fetcher_.reset();

  base::UmaHistogramEnumeration("ContextualTasks.WebUI.OAuthError",
                                error.state(),
                                GoogleServiceAuthError::NUM_STATES);

  if (error.state() != GoogleServiceAuthError::NONE) {
    // If this is a transient error, retry with exponential backoff.
    if (error.IsTransientError()) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: OnOAuthTokenReceived "
             "transient error, will retry";
      request_access_token_backoff_.InformOfRequest(false);
      base::TimeDelta delay =
          request_access_token_backoff_.GetTimeUntilRelease();
      if (delay.is_zero()) {
        OMNIBOX_LOG("nav_trace")
            << "ContextualTasks navigation trace: OnOAuthTokenReceived "
               "transient error, will retry immediately";
        StartAccessTokenFetch();
      } else {
        OMNIBOX_LOG("nav_trace")
            << "ContextualTasks navigation trace: OnOAuthTokenReceived "
               "transient error, will retry in "
            << base::NumberToString(delay.InSeconds()) << " seconds";
        token_refresh_timer_.Start(
            FROM_HERE, delay,
            base::BindOnce(&ContextualTasksUiService::StartAccessTokenFetch,
                           weak_ptr_factory_.GetWeakPtr(), std::nullopt));
      }
      return;
    }

    RecordOAuthMetrics(error, access_token_info);
    request_access_token_backoff_.Reset();
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: OnOAuthTokenReceived "
           "non-transient error, running callbacks with empty token";
    RunPendingAccessTokenCallbacks("");
    return;
  }
  RecordOAuthMetrics(error, access_token_info);
  request_access_token_backoff_.Reset();
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: OnOAuthTokenReceived "
         "success, running callbacks";
  RunPendingAccessTokenCallbacks(access_token_info.token);
}

void ContextualTasksUiService::RecordOAuthMetrics(
    GoogleServiceAuthError error,
    signin::AccessTokenInfo access_token_info) {
  if (!current_oauth_fetch_trigger_.has_value() ||
      fetch_start_time_.is_null()) {
    return;
  }

  base::TimeDelta latency = base::TimeTicks::Now() - fetch_start_time_;
  int tries = request_access_token_backoff_.failure_count() + 1;
  bool success = (error.state() == GoogleServiceAuthError::NONE);

  bool was_cached = false;
  if (success) {
    // Heuristic for cache hit.
    was_cached = (latency < kOAuthCacheHitThreshold);
  }

  // Record to .All
  base::UmaHistogramMediumTimes("ContextualTasks.OAuth.Latency.All", latency);
  base::UmaHistogramExactLinear("ContextualTasks.OAuth.TriesCount.All", tries,
                                10);
  if (success) {
    base::UmaHistogramExactLinear(
        "ContextualTasks.OAuth.TriesCountBeforeSuccess.All", tries, 10);
  } else {
    base::UmaHistogramExactLinear(
        "ContextualTasks.OAuth.TriesCountBeforeFailure.All", tries, 10);
  }
  base::UmaHistogramBoolean("ContextualTasks.OAuth.Success.All", success);
  if (success) {
    base::UmaHistogramBoolean("ContextualTasks.OAuth.WasCached.All",
                              was_cached);
  }

  // Record to .AimNavigation if applicable
  if (*current_oauth_fetch_trigger_ ==
      OAuthFetchTrigger::kAimNavigationInterception) {
    base::UmaHistogramMediumTimes("ContextualTasks.OAuth.Latency.AimNavigation",
                                  latency);
    base::UmaHistogramExactLinear(
        "ContextualTasks.OAuth.TriesCount.AimNavigation", tries, 10);
    if (success) {
      base::UmaHistogramExactLinear(
          "ContextualTasks.OAuth.TriesCountBeforeSuccess.AimNavigation", tries,
          10);
    } else {
      base::UmaHistogramExactLinear(
          "ContextualTasks.OAuth.TriesCountBeforeFailure.AimNavigation", tries,
          10);
    }
    base::UmaHistogramBoolean("ContextualTasks.OAuth.Success.AimNavigation",
                              success);
    if (success) {
      base::UmaHistogramBoolean("ContextualTasks.OAuth.WasCached.AimNavigation",
                                was_cached);
    }
  }

  // Reset trigger.
  current_oauth_fetch_trigger_ = std::nullopt;
  fetch_start_time_ = base::TimeTicks();
}

void ContextualTasksUiService::ShowOauthErrorDialogForWebContents(
    base::WeakPtr<content::WebContents> web_contents) {
  if (!web_contents) {
    return;
  }
  content::WebUI* webui = web_contents->GetWebUI();
  if (webui && webui->GetController() && webui->GetController()->GetType()) {
    auto* ui_controller = webui->GetController()->GetAs<ContextualTasksUI>();
    if (ui_controller) {
      ui_controller->ShowOauthErrorDialog();
    }
  }
}

void ContextualTasksUiService::RunPendingAccessTokenCallbacks(
    const std::string& token) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: "
         "RunPendingAccessTokenCallbacks called with token empty="
      << token.empty()
      << ", callbacks count=" << pending_access_token_callbacks_.size();
  std::vector<
      std::pair<GetAccessTokenCallback, base::WeakPtr<content::WebContents>>>
      callbacks;
  std::swap(callbacks, pending_access_token_callbacks_);

  if (token.empty()) {
    for (const auto& callback_pair : callbacks) {
      if (callback_pair.second) {
        content::WebContents* const wc = callback_pair.second.get();
        const bool is_tab = wc && tabs::TabInterface::MaybeGetFromContents(wc);
        if (lens::features::IsLensSidePanelUnificationEnabled() && !is_tab &&
            !IsSignedInToBrowserWithValidCredentials()) {
          // Bypass the OAuth error dialog for signed-out users under
          // unification in side panel mode.
          continue;
        }
        OMNIBOX_LOG("nav_trace")
            << "ContextualTasks navigation trace: "
               "RunPendingAccessTokenCallbacks showing oauth error dialog";
        ShowOauthErrorDialogForWebContents(callback_pair.second);
      }
    }
  }

  for (auto& callback_pair : callbacks) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: "
           "RunPendingAccessTokenCallbacks running callback";
    std::move(callback_pair.first).Run(token);
  }
}

void ContextualTasksUiService::OnEligibilityChanged(bool eligible) {
  // Trigger cookie sync only on a transition from false to true.
  if (eligible && !is_eligible_) {
    EnsureCookiesSynced();
  }
  is_eligible_ = eligible;
}

#if BUILDFLAG(ENABLE_PDF)
// Checks if the current URL is a scroll citation on the active tab. Returns
// true if this is a scroll citation and should be scrolled using the
// PDFDocumentHelper rather than opening normally.
static bool IsPdfCitation(const GURL& url,
                          tabs::TabInterface* active_tab,
                          const base::Uuid& task_id,
                          ContextualTasksService* service) {
  if (!GetIsContextualTasksPdfCitationsEnabled() || !active_tab) {
    return false;
  }
  content::WebContents* web_contents = active_tab->GetContents();
  std::optional<ContextualTask> task = service->GetContextualTaskForTab(
      SessionTabHelper::IdForTab(web_contents));
  if (!task || task->GetTaskId() != task_id) {
    return false;
  }
  const GURL& page_url = web_contents->GetLastCommittedURL();
  if (page_url.GetScheme() != url.GetScheme() ||
      page_url.GetHost() != url.GetHost() ||
      page_url.GetPath() != url.GetPath() ||
      page_url.GetQuery() != url.GetQuery() || url.GetRef().empty()) {
    return false;
  }
  return pdf::PDFDocumentHelper::MaybeGetForWebContents(*web_contents) !=
         nullptr;
}
#endif

#if !BUILDFLAG(IS_ANDROID)
// Checks if the current URL is a video citation on the active tab. Returns
// true if this is a video citation and should be handled by scrolling the
// video to the timestamp in the URL.
static bool IsVideoCitation(const GURL& url,
                            tabs::TabInterface* active_tab,
                            const base::Uuid& task_id,
                            ContextualTasksService* service) {
  if (!base::FeatureList::IsEnabled(kContextualTasksVideoCitations) ||
      !active_tab) {
    return false;
  }
  content::WebContents* web_contents = active_tab->GetContents();
  std::optional<ContextualTask> task = service->GetContextualTaskForTab(
      SessionTabHelper::IdForTab(web_contents));
  if (task && task->GetTaskId() == task_id) {
    return url.GetHost() == "www.youtube.com";
  }
  return false;
}
#endif

// Checks if the current URL is already open in the tab list. If so, returns a
// pointer to that tab. If there is not existing tab with the given url, returns
// nullptr.
static tabs::TabInterface* GetExistingTab(const GURL& url,
                                          TabListInterface* tab_list,
                                          const base::Uuid& task_id,
                                          ContextualTasksService* service) {
  if (!tab_list) {
    return nullptr;
  }
  GURL url_no_fragments =
      shared_highlighting::RemoveFragmentSelectorDirectives(url);
  for (int i = 0; i < tab_list->GetTabCount(); ++i) {
    tabs::TabInterface* tab = tab_list->GetTab(i);
    content::WebContents* web_contents = tab ? tab->GetContents() : nullptr;
    if (!web_contents) {
      continue;
    }
    std::optional<ContextualTask> task = service->GetContextualTaskForTab(
        SessionTabHelper::IdForTab(web_contents));
    GURL tab_url_no_fragments =
        shared_highlighting::RemoveFragmentSelectorDirectives(
            web_contents->GetLastCommittedURL());
    if (tab_url_no_fragments == url_no_fragments && task &&
        task->GetTaskId() == task_id) {
      return tab_list->GetTab(i);
    }
  }
  return nullptr;
}

// Moves the tab focus to the tab with `url` already open. If a tab is focused,
// returns a pointer to that tab. If no such tab exists and no focus was
// changed, this method returns nullptr.
tabs::TabInterface* ContextualTasksUiService::MaybeFocusExistingOpenTab(
    const GURL& url,
    TabListInterface* tab_list,
    const base::Uuid& task_id) {
  tabs::TabInterface* existing_tab =
      GetExistingTab(url, tab_list, task_id, contextual_tasks_service_);
  if (existing_tab) {
    tab_list->ActivateTab(existing_tab->GetHandle());
  }
  return existing_tab;
}

bool ContextualTasksUiService::MaybeHandleVideoCitation(
    const GURL& url,
    tabs::TabInterface* tab,
    const base::Uuid& task_id) {
#if !BUILDFLAG(IS_ANDROID)
  if (!IsVideoCitation(url, tab, task_id, contextual_tasks_service_)) {
    return false;
  }

  content::WebContents* web_contents = tab->GetContents();
  auto handler = CreateMediaLinkHandler(web_contents);
  if (handler && handler->MaybeReplaceNavigation(url)) {
    return true;
  }
#endif
  return false;
}

bool ContextualTasksUiService::MaybeHandlePdfCitation(
    const GURL& url,
    tabs::TabInterface* tab,
    const base::Uuid& task_id) {
#if BUILDFLAG(ENABLE_PDF)
  if (!IsPdfCitation(url, tab, task_id, contextual_tasks_service_)) {
    return false;
  }

  content::WebContents* web_contents = tab->GetContents();
  // Dispatch an event to the PDF viewer to update the viewport.
  pdf_extension_util::DispatchShouldUpdateViewportEvent(
      web_contents->GetPrimaryMainFrame(), url);
  return true;
#else
  return false;
#endif
}

void ContextualTasksUiService::OnThreadLinkClicked(
    const GURL& url,
    base::Uuid task_id,
    base::WeakPtr<tabs::TabInterface> tab,
    base::WeakPtr<BrowserWindowInterface> browser,
    const url::Origin& initiator_origin) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: OnThreadLinkClicked called "
         "for URL: "
      << url;
  if (!browser) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: OnThreadLinkClicked "
           "returning early, no browser";
    return;
  }

  TabListInterface* tab_list = TabListInterface::From(browser.get());

  std::string ai_response_link_clicked_metric_name =
      base::StrCat({"ContextualTasks.AiResponse.UserAction.LinkClicked.",
                    (tab ? "Tab" : "Panel")});
  base::UmaHistogramBoolean(ai_response_link_clicked_metric_name, true);
  base::RecordAction(
      base::UserMetricsAction(ai_response_link_clicked_metric_name.c_str()));

  // If the thread link click corresponds to a citation, it should be handled
  // before an unneeded web contents is created below.
  if (!tab) {
    tabs::TabInterface* active_tab = tab_list->GetActiveTab();
    if (MaybeHandleVideoCitation(url, active_tab, task_id) ||
        MaybeHandlePdfCitation(url, active_tab, task_id)) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: OnThreadLinkClicked "
             "citation handled on active tab";
      if (auto* controller =
              ContextualTasksPanelController::From(browser.get())) {
        controller->OnAiInteraction();
      }
      return;
    }
  }

  content::WebContents::CreateParams create_params(profile_);
  std::unique_ptr<content::WebContents> message_proxy_web_contents;

  if (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_) {
    message_proxy_web_contents =
        CreateMessageProxyWebContents(initiator_origin);
    create_params.opener_id =
        message_proxy_web_contents->GetPrimaryMainFrame()->GetGlobalId();
  }

  std::unique_ptr<content::WebContents> new_contents =
      content::WebContents::Create(create_params);
  content::WebContents* new_contents_ptr = new_contents.get();
  CreateSessionServiceTabHelper(new_contents_ptr);

  if (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_) {
    tracker_manager_->MatchAndAssociatePendingTracker(
        url, new_contents_ptr, std::move(message_proxy_web_contents));
  }

  // Copy navigation entries from the current tab to the new tab to support back
  // button navigation. See crbug.com/467042329 for detail.
  if (tab) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: OnThreadLinkClicked "
           "copying navigation entries from tab";
    new_contents->GetController().CopyStateFrom(
        &tab->GetContents()->GetController(), /*needs_reload=*/false);
  }

  // If the source contents is the panel, open the AI page in a new foreground
  // tab.
  // TODO(crbug.com/458139141): Split this API so we can assume `tab` non-null.
  if (!tab) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: OnThreadLinkClicked source "
           "is panel, opening in tab";
    // Attempt to focus an existing tab prior to creating a new one.
    tabs::TabInterface* existing_tab = nullptr;
    existing_tab = MaybeFocusExistingOpenTab(url, tab_list, task_id);
    if (!existing_tab) {
      if (task_id.is_valid()) {
        AssociateWebContentsToTask(new_contents_ptr, task_id);
      }

      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: OnThreadLinkClicked "
             "using InsertWebContentsAt";
      // Insert the WebContents after the current active.
      int active_tab_index = tab_list->GetActiveIndex();
      tabs::TabInterface* active_tab = tab_list->GetActiveTab();
      tabs::TabInterface* new_tab = tab_list->InsertWebContentsAt(
          active_tab_index + 1, std::move(new_contents),
          /*should_pin=*/false,
          /*group=*/active_tab ? active_tab->GetGroup() : std::nullopt);

      // Set the opener relationship so that if the new tab is closed, focus
      // returns to the active tab.
      if (active_tab) {
        tab_list->SetOpenerForTab(new_tab->GetHandle(),
                                  active_tab->GetHandle());
      }
      tab_list->ActivateTab(new_tab->GetHandle());

      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: OnThreadLinkClicked "
             "loading URL in inserted tab: "
          << url;
      content::NavigationController::LoadURLParams params(url);
      params.override_user_agent =
          content::NavigationController::UA_OVERRIDE_TRUE;
      new_tab->GetContents()->GetController().LoadURLWithParams(params);
    } else {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: OnThreadLinkClicked "
             "existing tab found";
      // If the tab was found, check if there was a text fragment to search for
      // in the URL. If so, highlight them to be shown to the user.
      std::vector<std::string> fragments =
          shared_highlighting::ExtractTextFragments(url.GetRef());

      content::Page& page = existing_tab->GetContents()->GetPrimaryPage();
      companion::TextFinderManager* text_finder_manager =
          companion::TextFinderManager::GetOrCreateForPage(page);
      text_finder_manager->CreateTextFinders(
          fragments,
          base::BindOnce(&ContextualTasksUiService::OnTextFinderLookupComplete,
                         weak_ptr_factory_.GetWeakPtr(),
                         existing_tab->GetWeakPtr(), url, task_id, browser));
    }

    if (auto* controller =
            ContextualTasksPanelController::From(browser.get())) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: OnThreadLinkClicked "
             "counting as part of a cobrowsing session";
      // Count as part of a cobrowsing session if the user interacted with the
      // AI response.
      controller->OnAiInteraction();
    }

    return;
  }

  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: OnThreadLinkClicked source "
         "is tab, opening linked page in tab directly after";

  // Get the index of the web contents.
  const int current_index = tab_list->GetIndexOfTab(tab->GetHandle());

  // Open the linked page in a tab directly after this one.
  // To prevent side panel to close and reopen again, add the new tab, associate
  // with task and then activate it.
  tabs::TabInterface* new_tab =
      tab_list->InsertWebContentsAt(current_index + 1, std::move(new_contents),
                                    /*should_pin=*/false, tab->GetGroup());

  AssociateWebContentsToTask(new_contents_ptr, task_id);

  // Set the opener relationship so that if the new tab is closed, focus
  // returns to the source tab.
  tab_list->SetOpenerForTab(new_tab->GetHandle(), tab->GetHandle());

  tab_list->ActivateTab(new_tab->GetHandle());
  CHECK(new_contents_ptr == tab_list->GetActiveTab()->GetContents());

  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: OnThreadLinkClicked "
         "loading URL in inserted tab: "
      << url;
  content::NavigationController::LoadURLParams params(url);
  params.override_user_agent = content::NavigationController::UA_OVERRIDE_TRUE;
  new_tab->GetContents()->GetController().LoadURLWithParams(params);

  // Detach the WebContents from tab.
  std::unique_ptr<content::WebContents> contextual_task_contents =
      tab_list->DetachWebContents(tab->GetHandle());
  content::WebContents* contextual_task_contents_ptr =
      contextual_task_contents.get();

  // Transfer the contextual task contents into the side panel cache.
  ContextualTasksPanelController::From(browser.get())
      ->TransferWebContentsFromTab(task_id,
                                   std::move(contextual_task_contents));

  // Open the side panel.
  ContextualTasksPanelController::From(browser.get())
      ->Show(/*transition_from_tab=*/true);

  // Notify the WebUI to adjust itself e.g. hide the toolbar.
  // `contextual_task_contents_ptr` is guaranteed to be alive here, since
  // the ownership of `contextual_task_contents` has been moved to
  // the ContextualTasksPanelController implementation's instance.
  if (auto* web_ui_interface =
          GetWebUiInterface(contextual_task_contents_ptr)) {
    web_ui_interface->OnSidePanelStateChanged();
  }
}

void ContextualTasksUiService::OnTextFinderLookupComplete(
    base::WeakPtr<tabs::TabInterface> tab,
    const GURL& url,
    base::Uuid task_id,
    base::WeakPtr<BrowserWindowInterface> browser,
    const std::vector<std::pair<std::string, bool>>& lookup_results) {
  if (!browser) {
    return;
  }

  bool all_text_found = true;
  std::vector<std::string> text_directives;
  for (const auto& pair : lookup_results) {
    if (!pair.second) {
      all_text_found = false;
      break;
    }
    text_directives.push_back(pair.first);
  }

  if (!tab || !all_text_found) {
    // If the tab went away or the text wasn't found on the page, open a new
    // tab.
    std::unique_ptr<content::WebContents> new_contents =
        content::WebContents::Create(
            content::WebContents::CreateParams(profile_));
    content::WebContents* new_contents_ptr = new_contents.get();
    CreateSessionServiceTabHelper(new_contents_ptr);

    new_contents->GetController().LoadURLWithParams(
        content::NavigationController::LoadURLParams(url));

    AssociateWebContentsToTask(new_contents_ptr, task_id);

    TabListInterface* tab_list = TabListInterface::From(browser.get());
    // Insert the WebContents after the current active.
    int active_tab_index = tab_list->GetActiveIndex();
    tabs::TabInterface* active_tab = tab_list->GetActiveTab();
    tabs::TabInterface* new_tab = tab_list->InsertWebContentsAt(
        active_tab_index + 1, std::move(new_contents),
        /*should_pin=*/false,
        /*group=*/active_tab ? active_tab->GetGroup() : std::nullopt);
    if (active_tab) {
      tab_list->SetOpenerForTab(new_tab->GetHandle(), active_tab->GetHandle());
    }
    tab_list->ActivateTab(new_tab->GetHandle());
    return;
  }

  // Delete any existing `TextHighlighterManager` on the page. Without this, any
  // text highlights after the first to be rendered on the page will not render.
  auto& page = tab->GetContents()->GetPrimaryPage();
  if (companion::TextHighlighterManager::GetForPage(page)) {
    companion::TextHighlighterManager::DeleteForPage(page);
  }

  // If every text fragment was found, then create a text highlighter manager to
  // render the text highlights. Focus the main tab first.
  tab->GetContents()->Focus();
  companion::TextHighlighterManager* text_highlighter_manager =
      companion::TextHighlighterManager::GetOrCreateForPage(page);
  text_highlighter_manager->CreateTextHighlightersAndRemoveExisting(
      text_directives);
}

bool ContextualTasksUiService::ShouldAllowNewTabOpen(
    const GURL& url,
    BrowserWindowInterface* browser,
    const base::Uuid& task_id) {
  TabListInterface* tab_list =
      browser ? TabListInterface::From(browser) : nullptr;
#if BUILDFLAG(ENABLE_PDF) || !BUILDFLAG(IS_ANDROID)
  tabs::TabInterface* active_tab =
      tab_list ? tab_list->GetActiveTab() : nullptr;
#endif

#if BUILDFLAG(ENABLE_PDF)
  if (IsPdfCitation(url, active_tab, task_id, contextual_tasks_service_)) {
    return false;
  }
#endif

#if !BUILDFLAG(IS_ANDROID)
  if (IsVideoCitation(url, active_tab, task_id, contextual_tasks_service_)) {
    return false;
  }
#endif

  if (GetExistingTab(url, tab_list, task_id, contextual_tasks_service_)) {
    return false;
  }

  return true;
}

void ContextualTasksUiService::InitializeTaskInSidePanel(
    content::WebContents* web_contents,
    const base::Uuid& task_id,
    std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
        session_handle) {
  // Retrieve the session handle from pending session handles if it was stored
  // early to prevent race conditions where the NavigationThrottle checks
  // eligibility before the handle is associated with the WebContents.
  if (!session_handle) {
    auto it = pending_session_handles_.find(task_id);
    if (it != pending_session_handles_.end()) {
      session_handle = std::move(it->second);
      pending_session_handles_.erase(it);
    }
  }
  if (session_handle) {
    ContextualSearchWebContentsHelper::GetOrCreateForWebContents(web_contents)
        ->SetTaskSession(task_id, std::move(session_handle),
                         /*input_state_model=*/nullptr);
  }
  AssociateWebContentsToTask(web_contents, task_id);
}

void ContextualTasksUiService::OnNonThreadNavigationInTab(
    content::OpenURLParams url_params,
    base::WeakPtr<tabs::TabInterface> tab) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: OnNonThreadNavigationInTab "
         "called for URL: "
      << url_params.url;
  if (!tab || !tab->GetContents()) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: OnNonThreadNavigationInTab "
           "returning early, no tab";
    return;
  }

  content::NavigationController::LoadURLParams params(url_params);
  params.transition_type = ::ui::PAGE_TRANSITION_AUTO_TOPLEVEL;
  params.frame_tree_node_id = content::FrameTreeNodeId();
  tab->GetContents()->GetController().LoadURLWithParams(params);
}

void ContextualTasksUiService::OnSearchResultsNavigationInSidePanel(
    content::OpenURLParams url_params,
    ContextualTasksUIInterface* web_ui_interface) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: "
         "OnSearchResultsNavigationInSidePanel called for URL: "
      << url_params.url;
  web_ui_interface->TransferNavigationToEmbeddedPage(url_params);
}

bool ContextualTasksUiService::ShouldRedirectIneligibleRequest(
    const GURL& url,
    content::WebContents* source_contents) const {
  // If it's a top-level frame refresh/navigation while viewing an internal
  // context, and the user environment isn't eligible, bounce immediately.
  if (eligibility_manager_ && eligibility_manager_->IsEligible()) {
    return false;
  }

  // If the user is signed in and eligible without identity, but identity token
  // loading is still pending (e.g. during cold start on Android), do not
  // redirect yet.
  if (eligibility_manager_ &&
      eligibility_manager_->IsEligibleWithoutIdentity() && identity_manager_ &&
      identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin) &&
      !identity_manager_->AreRefreshTokensLoaded()) {
    return false;
  }

  base::Uuid task_id;
  std::string task_id_str;
  if (net::GetValueForKeyInQuery(url, kTaskQueryParam, &task_id_str)) {
    task_id = base::Uuid::ParseLowercase(task_id_str);
  }

  // Bypasses the redirect check if the session was started from Lens and the
  // Lens side panel unification feature is enabled (either as an active
  // session or a pending session for the given task ID), or if the active tab
  // is present in context on the WebContents.
  if (IsSessionAllowedWhileIneligible(source_contents, task_id) ||
      IsActiveTabInContext(source_contents)) {
    return false;
  }

  if (!IsContextualTasksUrl(url)) {
    return false;
  }
  // Don't intercept internal debugging tools
  if (url.path() == "/internals") {
    return false;
  }
  // Don't intercept WebUI tests
  if (url.path() == "/test_loader.html" ||
      base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kBrowserTest)) {
    return false;
  }

  return true;
}

bool ContextualTasksUiService::IsSessionAllowedWhileIneligible(
    content::WebContents* web_contents,
    const base::Uuid& task_id) const {
  if (!lens::features::IsLensSidePanelUnificationEnabled()) {
    return false;
  }

  // Allow cobrowse session if we reached here from lens entry points. It will
  // end up in opening the side panel.
  if (web_contents) {
    auto* helper =
        ContextualSearchWebContentsHelper::FromWebContents(web_contents);
    auto* session_handle = helper ? helper->session_handle() : nullptr;
    if (session_handle) {
      auto* metrics_recorder = session_handle->GetMetricsRecorder();
      if (metrics_recorder &&
          metrics_recorder->source() ==
              contextual_search::ContextualSearchSource::kLens) {
        return true;
      }
    }
  }

  if (task_id.is_valid()) {
    auto it = pending_session_handles_.find(task_id);
    if (it != pending_session_handles_.end()) {
      auto* session_handle = it->second.get();
      if (session_handle) {
        auto* metrics_recorder = session_handle->GetMetricsRecorder();
        if (metrics_recorder &&
            metrics_recorder->source() ==
                contextual_search::ContextualSearchSource::kLens) {
          return true;
        }
      }
    }
  }

  return false;
}

void ContextualTasksUiService::AddPendingSessionHandleForTesting(  // IN-TEST
    const base::Uuid& task_id,
    std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
        session_handle) {
  pending_session_handles_.emplace(task_id, std::move(session_handle));
}

bool ContextualTasksUiService::HandleNavigation(
    content::OpenURLParams url_params,
    content::WebContents* source_contents,
    bool is_from_embedded_page,
    bool from_can_create_window,
    bool is_same_site_or_from_ui,
    bool is_mobile_ua,
    const std::optional<url::Origin>& initiator_origin,
    const std::optional<content::GlobalRenderFrameHostToken>&
        initiator_frame_token,
    const blink::mojom::WindowFeatures& window_features) {
  if (contextual_tasks::IsContextualTasksRearchitectureEnabled() ||
      contextual_tasks::IsContextualTasksSidePanelRearchitectureEnabled()) {
    return HandleNavigationImplPostRearchitecture(
        std::move(url_params), source_contents,
        tabs::TabInterface::MaybeGetFromContents(source_contents),
        is_from_embedded_page, from_can_create_window, is_same_site_or_from_ui,
        is_mobile_ua, initiator_origin, initiator_frame_token, window_features);
  }
  return HandleNavigationImpl(
      std::move(url_params), source_contents,
      tabs::TabInterface::MaybeGetFromContents(source_contents),
      is_from_embedded_page, from_can_create_window, is_same_site_or_from_ui,
      is_mobile_ua, initiator_origin, initiator_frame_token, window_features);
}

bool ContextualTasksUiService::HandleNavigationImplPostRearchitecture(
    content::OpenURLParams url_params,
    content::WebContents* source_contents,
    tabs::TabInterface* tab,
    bool is_from_embedded_page,
    bool from_can_create_window,
    bool is_same_site_or_from_ui,
    bool is_mobile_ua,
    const std::optional<url::Origin>& initiator_origin,
    const std::optional<content::GlobalRenderFrameHostToken>&
        initiator_frame_token,
    const blink::mojom::WindowFeatures& window_features) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks HandleNavigationImplPostRearchitecture: "
      << url_params.url.spec();
  // Check if the navigation originates from the side panel WebContents and
  // requires URL changes (e.g. forced host override, missing parameters) to be
  // applied.
  if (ShouldAddRequiredSidePanelUrlChanges(url_params, source_contents)) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks HandleNavigationImplPostRearchitecture: adding url "
           "changes";
    return AddRequiredSidePanelUrlChanges(std::move(url_params),
                                          source_contents);
  }

  return false;
}

bool ContextualTasksUiService::ShouldAddRequiredSidePanelUrlChanges(
    const content::OpenURLParams& url_params,
    content::WebContents* source_contents) {
  if (!source_contents) {
    return false;
  }

  // Retrieve the BrowserWindowInterface registered on the side panel
  // WebContents.
  BrowserWindowInterface* browser_window =
      webui::GetBrowserWindowInterface(source_contents);
  if (!browser_window) {
    return false;
  }

  // If not panel open on this browser window, then source_contents cannot be in
  // the side panel.
  auto* controller = ContextualTasksPanelController::From(browser_window);
  if (!controller) {
    return false;
  }

  // Verify that source_contents is explicitly a side panel WebContents.
  bool is_side_panel_contents = false;
  for (auto* wc : controller->GetPanelWebContentsList()) {
    if (wc == source_contents) {
      is_side_panel_contents = true;
      break;
    }
  }
  if (!is_side_panel_contents) {
    return false;
  }

  const GURL& url = url_params.url;

  // Only apply to HTTP and HTTPS URLs.
  if (!url.SchemeIsHTTPOrHTTPS()) {
    return false;
  }

  // Do not intercept Google CAPTCHA challenge URLs.
  if (IsGoogleCaptchaUrl(url)) {
    return false;
  }

  // Only apply to Search or AIM/Lens search URLs.
  if (!IsSearchResultsUrl(url) && !IsAiUrl(url)) {
    return false;
  }

  // Check if host override is set and needs to be applied.
  std::string forced_host = GetForcedEmbeddedPageHost();
  if (!forced_host.empty() &&
      !base::EqualsCaseInsensitiveASCII(url.host(), forced_host) &&
      !IsSignInDomain(url)) {
    OMNIBOX_LOG("nav_trace")
        << "ShouldAddRequiredSidePanelUrlChanges: host mismatch: "
        << std::string(url.host()) << " vs forced " << forced_host;
    return true;
  }

  // Retrieve expected common search parameters (gsc=2, hl, cs, gl=us, etc.).
  auto expected_params =
      GetCommonSearchParamsMapForContextualTasks(source_contents);

  // Check if any expected parameter is missing, or if 'cs' (dark mode) or
  // 'gsc' (side panel) is out of sync.
  for (const auto& [key, value] : expected_params) {
    std::string current_val;
    bool has_key = net::GetValueForKeyInQuery(url, key, &current_val);
    if (!has_key) {
      OMNIBOX_LOG("nav_trace")
          << "ShouldAddRequiredSidePanelUrlChanges: missing param " << key;
      return true;  // Missing parameter -> needs handling.
    }
    if (key == lens::kChromeSidePanelParameterKey && current_val != value) {
      OMNIBOX_LOG("nav_trace")
          << "ShouldAddRequiredSidePanelUrlChanges: " << key
          << " value mismatch";
      return true;
    }
  }

  return false;
}

bool ContextualTasksUiService::AddRequiredSidePanelUrlChanges(
    content::OpenURLParams url_params,
    content::WebContents* source_contents) {
  if (!source_contents) {
    return false;
  }

  GURL new_url =
      AddRequiredSidePanelUrlChanges(url_params.url, source_contents);

  OMNIBOX_LOG("nav_trace")
      << "AddRequiredSidePanelUrlChanges: loading parameterized URL: "
      << new_url.spec();

  // Load the parameterized URL into the side panel WebContents asynchronously
  // to avoid re-entrancy issues with NavigationControllerImpl while a
  // navigation is in progress.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](base::WeakPtr<content::WebContents> web_contents,
             content::OpenURLParams params, GURL url) {
            if (!web_contents) {
              return;
            }
            content::NavigationController::LoadURLParams load_params(url);
            load_params.referrer = params.referrer;
            load_params.transition_type = params.transition;
            web_contents->GetController().LoadURLWithParams(load_params);
          },
          source_contents->GetWeakPtr(), url_params, new_url));

  return true;
}

GURL ContextualTasksUiService::AddRequiredSidePanelUrlChanges(
    const GURL& url,
    content::WebContents* source_contents) {
  if (!source_contents || !url.SchemeIsHTTPOrHTTPS()) {
    return url;
  }

  auto common_params =
      GetCommonSearchParamsMapForContextualTasks(source_contents);

  GURL new_url = url;
  for (const auto& [key, value] : common_params) {
    std::string current_val;
    // Skip existing keys except 'gsc'.
    if (key != lens::kChromeSidePanelParameterKey &&
        net::GetValueForKeyInQuery(new_url, key, &current_val)) {
      continue;
    }
    if (value.empty()) {
      new_url = net::AppendOrReplaceQueryParameter(new_url, key, std::nullopt);
    } else {
      new_url = net::AppendOrReplaceQueryParameter(new_url, key, value);
    }
  }

  std::string forced_host = GetForcedEmbeddedPageHost();
  if (!forced_host.empty() && !IsSignInDomain(new_url)) {
    GURL::Replacements replacements;
    replacements.SetHostStr(forced_host);
    new_url = new_url.ReplaceComponents(replacements);
  }

  return new_url;
}

std::map<std::string, std::string>
ContextualTasksUiService::GetCommonSearchParamsMapForContextualTasks(
    content::WebContents* source_contents) {
  if (!source_contents) {
    return {};
  }

  bool is_dark_mode = false;
  // TODO(crbug.com/536100150): Add support for Desktop Android
#if !BUILDFLAG(IS_ANDROID)
  Profile* profile =
      Profile::FromBrowserContext(source_contents->GetBrowserContext());
  is_dark_mode = contextual_tasks::ShouldUseDarkMode(profile);
#endif

  bool is_side_panel =
      tabs::TabInterface::MaybeGetFromContents(source_contents) == nullptr;
  if (contextual_tasks::ShouldForceGscInTabMode()) {
    is_side_panel = true;
  }

  std::string country_code = g_browser_process->GetApplicationLocale();
  if (contextual_tasks::ShouldForceCountryCodeUS()) {
    country_code = "US";
  }

  auto params = lens::GetCommonSearchParametersMap(country_code, is_dark_mode,
                                                   is_side_panel);
  if (contextual_tasks::ShouldForceCountryCodeUS()) {
    params["gl"] = "us";
  }
  return params;
}

void ContextualTasksUiService::GetAccessToken(
    GetAccessTokenCallback callback,
    base::WeakPtr<content::WebContents> web_contents) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: GetAccessToken called";
  pending_access_token_callbacks_.emplace_back(std::move(callback),
                                               web_contents);
  StartAccessTokenFetch(OAuthFetchTrigger::kFetchRequest);
}

void ContextualTasksUiService::StartAccessTokenFetch(
    std::optional<OAuthFetchTrigger> trigger) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: StartAccessTokenFetch called";

  // If a request is already in progress, or we are waiting to retry, do
  // nothing.
  if (access_token_fetcher_ || token_refresh_timer_.IsRunning()) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: StartAccessTokenFetch returning "
           "early because fetch is in progress or waiting to retry";
    return;
  }

  if (trigger.has_value() && !current_oauth_fetch_trigger_.has_value()) {
    current_oauth_fetch_trigger_ = trigger;
    fetch_start_time_ = base::TimeTicks::Now();
    base::UmaHistogramBoolean("ContextualTasks.OAuth.Start.All", true);
    if (*trigger == OAuthFetchTrigger::kAimNavigationInterception) {
      base::UmaHistogramBoolean("ContextualTasks.OAuth.Start.AimNavigation",
                                true);
    }
  }

  token_refresh_timer_.Stop();

  if (!identity_manager_ ||
      !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: StartAccessTokenFetch "
           "returning early due to no primary account";
    if (current_oauth_fetch_trigger_.has_value()) {
      RecordOAuthMetrics(GoogleServiceAuthError::CreateAccountNotFound(),
                         signin::AccessTokenInfo());
    }
    RunPendingAccessTokenCallbacks("");
    return;
  }

  auto account =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);

  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: StartAccessTokenFetch "
         "creating access token fetcher";
  access_token_fetcher_ = identity_manager_->CreateAccessTokenFetcherForAccount(
      account.account_id, signin::OAuthConsumerId::kContextualTasks,
      base::BindOnce(&ContextualTasksUiService::OnOAuthTokenReceived,
                     weak_ptr_factory_.GetWeakPtr()),
      signin::AccessTokenFetcher::Mode::kWaitUntilRefreshTokenAvailable);
}

std::unique_ptr<content::WebContents>
ContextualTasksUiService::CreateMessageProxyWebContents(
    const url::Origin& origin) {
  content::WebContents::CreateParams dummy_params(profile_);
  dummy_params.site_instance =
      content::SiteInstance::CreateForURL(profile_, origin.GetURL());
  auto message_proxy_web_contents = content::WebContents::Create(dummy_params);

  GuestOpenerUserData::CreateForWebContents(message_proxy_web_contents.get());

  content::NavigationController::LoadURLParams load_params(GURL("about:blank"));
  // A CHECK will fail if setting the origin to an invalid URL.
  if (origin.GetURL().is_valid()) {
    load_params.initiator_origin = origin;
    load_params.initiator_base_url = origin.GetURL();
  }
  message_proxy_web_contents->GetController().LoadURLWithParams(load_params);
  return message_proxy_web_contents;
}

void ContextualTasksUiService::OpenUrl(
    const content::OpenURLParams& url_params,
    const blink::mojom::WindowFeatures& window_features,
    BrowserWindowInterface* browser) {
  const GURL& url = url_params.url;
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: OpenUrl called "
         "for URL: "
      << url << ", disposition: " << static_cast<int>(url_params.disposition);

  NavigateParams nav_params(profile_, url, url_params.transition);
  nav_params.FillNavigateParamsFromOpenURLParams(url_params);

  // Browser and tab index have no equivalent in OpenURLParams, so add them to
  // ensure the tab opens in the right tab strip position.
  if (browser) {
    nav_params.browser = browser;
    TabListInterface* tab_list = TabListInterface::From(browser);
    if (tab_list) {
      nav_params.tabstrip_index = tab_list->GetActiveIndex() + 1;
    }
  }
  // Reset frame_tree_node_id to avoid targeting the source frame when opening
  // a new tab/window.
  nav_params.frame_tree_node_id = content::FrameTreeNodeId();

  // Apply window features (bounds).
  nav_params.window_features = window_features;
  const auto& bounds = nav_params.window_features.bounds;
  OMNIBOX_LOG("nav_trace") << "OpenUrl: applied window features: bounds=["
                           << bounds.x() << "," << bounds.y() << " "
                           << bounds.width() << "x" << bounds.height() << "]";

  std::unique_ptr<content::WebContents> message_proxy_web_contents;
  if (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_ &&
      base::FeatureList::IsEnabled(kAimTriggeredThreadLinks) &&
      url_params.initiator_origin.has_value()) {
    OMNIBOX_LOG("nav_trace") << "ContextualTasks navigation trace: OpenUrl "
                                "setting opener for initiator_origin: "
                             << url_params.initiator_origin->Serialize();

    // Create the dummy contents to be used as opener.
    message_proxy_web_contents =
        CreateMessageProxyWebContents(*url_params.initiator_origin);

    nav_params.opener = message_proxy_web_contents->GetPrimaryMainFrame();
  }

  // Perform the navigation
  Navigate(&nav_params);

  // Grab the web contents that was associated with tis navigation and begin
  // tracking it.
  content::WebContents* new_contents_ptr =
      nav_params.navigated_or_inserted_contents;

  if (new_contents_ptr) {
    if (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_) {
      tracker_manager_->MatchAndAssociatePendingTracker(
          url, new_contents_ptr, std::move(message_proxy_web_contents));
    }
  } else {
    OMNIBOX_LOG("nav_trace") << "ContextualTasks navigation trace: "
                                "OpenUrl failed to get new WebContents";
  }
}

bool ContextualTasksUiService::HandleNavigationImpl(
    content::OpenURLParams url_params,
    content::WebContents* source_contents,
    tabs::TabInterface* tab,
    bool is_from_embedded_page,
    bool from_can_create_window,
    bool is_same_site_or_from_ui,
    bool is_mobile_ua,
    const std::optional<url::Origin>& initiator_origin,
    const std::optional<content::GlobalRenderFrameHostToken>&
        initiator_frame_token,
    const blink::mojom::WindowFeatures& window_features) {
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: HandleNavigationImpl called "
         "for URL: "
      << url_params.url << ", is_from_embedded_page=" << is_from_embedded_page
      << ", from_can_create_window=" << from_can_create_window
      << ", has_tab=" << (tab != nullptr)
      << ", disposition=" << static_cast<int>(url_params.disposition)
      << ", initiator_origin="
      << (url_params.initiator_origin.has_value()
              ? url_params.initiator_origin->Serialize()
              : "null");

  if (!is_from_embedded_page &&
      ShouldRedirectIneligibleRequest(url_params.url, source_contents)) {
    ScheduleRedirectWebUIUrlToAim(
        std::move(url_params),
        source_contents ? source_contents->GetWeakPtr() : nullptr, tab);
    return true;
  }

  // Whether the navigation is from forward navigation and originally from
  // a link click. `page_transition` can contain both the original navigation
  // information (from link click or typed, etc) and the modified one(from
  // forward/back).
  ui::PageTransition page_transition = url_params.transition;
  bool is_forward_link_navigation =
      (page_transition & ui::PAGE_TRANSITION_FORWARD_BACK) && source_contents &&
      (source_contents->GetController().GetPendingEntryIndex() >
       source_contents->GetController().GetLastCommittedEntryIndex()) &&
      (ui::PageTransitionCoreTypeIs(page_transition,
                                    ui::PAGE_TRANSITION_LINK) ||
       ui::PageTransitionCoreTypeIs(page_transition,
                                    ui::PAGE_TRANSITION_RELOAD));

  bool is_nav_within_existing_session =
      lens::features::IsLensSidePanelUnificationEnabled() &&
      (is_from_embedded_page || is_forward_link_navigation) &&
      source_contents &&
      IsContextualTasksUrl(source_contents->GetLastCommittedURL());

  // Make sure the user is eligible to use the feature before attempting to
  // intercept.
  if (!is_nav_within_existing_session &&
      (!eligibility_manager_ ||
       !eligibility_manager_->IsEligibleWithoutIdentity())) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: HandleNavigationImpl "
           "returning early, not eligible";
    return false;
  }

#if !BUILDFLAG(IS_ANDROID)
  if (is_mobile_ua) {
    // Exit cobrowsing if emulating a mobile device. Cobrowse on mobile is
    // dependent on Android to function correctly.
    if (IsContextualTasksUrl(url_params.url)) {
      // Redirect contextual tasks URL to aim page URL.
      GURL url = GetAiUrlFromWebUIUrl(GetDefaultAiPageUrl(), url_params.url);
      // Post a task to open the URL.
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(&ContextualTasksUiService::LoadUrlInWebContents,
                         weak_ptr_factory_.GetWeakPtr(), url,
                         source_contents->GetWeakPtr()));
      return true;
    }
    return false;
  }
#endif

  // If the target URL is a contextual tasks "display URL", then replace it with
  // the proper AIM URL.
  bool original_url_is_virtual = false;
  if (IsContextualTasksDisplayUrl(url_params.url)) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: HandleNavigationImpl "
           "navigating to contextual tasks display URL";
    original_url_is_virtual = true;
    const GURL aim_url =
        GetUrlForAim(TemplateURLServiceFactory::GetForProfile(profile_.get()),
                     omnibox::UNKNOWN_AIM_ENTRY_POINT, base::Time::Now(), u"",
                     std::nullopt, {});

    GURL::Replacements replacements;
    replacements.SetSchemeStr(aim_url.scheme());
    replacements.SetHostStr(aim_url.host());
    replacements.SetPathStr(aim_url.path());

    url_params.url = url_params.url.ReplaceComponents(replacements);
  }
  if (IsContextualTasksUrl(url_params.url)) {
    if (is_from_embedded_page) {
      DCHECK(false) << "Unexpected URL from embedded page " << url_params.url;
      return true;
    }

#if !BUILDFLAG(IS_ANDROID)
    // If the navigation is a back/forward navigation, the resulting URL is a
    // contextual tasks URL, and the panel is open, this indicates that the user
    // is navigating back to the full tab contextual tasks page. Instead of
    // allowing the navigation, move contextual tasks from the panel to a full
    // tab and close the current tab.
    bool is_forward_back_navigation =
        url_params.transition & ui::PAGE_TRANSITION_FORWARD_BACK;
    if (base::FeatureList::IsEnabled(
            kContextualTasksBackButtonExpandsSidePanel) &&
        !url_params.is_renderer_initiated && is_forward_back_navigation &&
        tab) {
      auto* controller = contextual_tasks::ContextualTasksPanelController::From(
          tab->GetBrowserWindowInterface());
      if (controller->IsPanelOpenForContextualTask()) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                &ContextualTasksUiService::OnBackButtonExpandsSidePanel,
                weak_ptr_factory_.GetWeakPtr(), tab->GetWeakPtr()));
        OMNIBOX_LOG("nav_trace")
            << "ContextualTasks navigation trace: HandleNavigationImpl "
               "closing tab and expanding side panel";
        return true;
      }
    }
#endif

    // Else, allow any navigation to the contextual tasks host.
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: HandleNavigationImpl "
           "returning early, navigating to contextual tasks host";
    return false;
  }

  bool is_nav_to_ai = IsAiUrl(url_params.url);

  // The "deb=nocobrowse1" and "ncb=1" params allow bypassing interception.
  bool should_bypass_interception = false;
  std::string bypass_reason;

  if (is_nav_to_ai) {
    should_bypass_interception =
        aim_eligibility_service_ &&
        aim_eligibility_service_->HasNoCobrowseParams(url_params.url);

    // If the page is to AI and the navigation is not same site, apply a param
    // to the URL to mark it as untrusted. Likewise, remove it if present and
    // the navigation is same site.
    if (!is_same_site_or_from_ui) {
      url_params.url =
          net::AppendOrReplaceQueryParameter(url_params.url, "cru", "1");
    } else {
      url_params.url = net::AppendOrReplaceQueryParameter(url_params.url, "cru",
                                                          std::nullopt);
    }
  }

  if (should_bypass_interception) {
    if (original_url_is_virtual) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: HandleNavigationImpl "
             "posting LoadUrlInWebContents";
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(&ContextualTasksUiService::LoadUrlInWebContents,
                         weak_ptr_factory_.GetWeakPtr(), url_params.url,
                         source_contents->GetWeakPtr()));
      return true;
    } else {
      return false;
    }
  }

  // If the user is not signed in to Chrome, do not intercept.
  if (!is_nav_within_existing_session &&
      !IsSignedInToBrowserWithValidCredentials()) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: HandleNavigationImpl "
           "returning false, not signed into browser";
    return false;
  }

  // If the user is not signed in to the account that is using the URL, do not
  // intercept.
  if (!is_nav_within_existing_session && is_nav_to_ai &&
      !IsUrlForPrimaryAccount(url_params.url)) {
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: HandleNavigationImpl "
           "returning false, not signed into account for AI URL";
    return false;
  }

  // At this point, the user is signed in to Chrome and signed into the account
  // that is using the URL. From here on out, the navigation can be intercepted.
  bool is_nav_to_sign_in = IsSignInDomain(url_params.url);

  BrowserWindowInterface* browser =
      tab ? tab->GetBrowserWindowInterface()
          : webui::GetBrowserWindowInterface(source_contents);

  // Retrieve the stored open url params from the pending tracker. If found, use
  // those params for this navigation. This is required as the second call to
  // HandleNavigationImpl after CanCreateWindow has the navigation params for
  // the navigation in the opened window, rather than the original params that
  // started the chain of naivgations.
  ContextualTasksWindowTracker* pending_tracker =
      (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_)
          ? tracker_manager_->GetPendingTracker(url_params.url, source_contents)
          : nullptr;
  if (pending_tracker && pending_tracker->open_url_params()) {
    // When the navigation goes through CanCreateWindow, the first navigation
    // has the correct url params to trigger the new window, where as the second
    // has the correct initiator and other url params. Therefore, copy the
    // disposition and other params that dictate where the navigation goes into
    // the url params for the second navigation to make sure the navigation
    // opens in the correct place.
    const auto& stored_params = *pending_tracker->open_url_params();
    url_params.disposition = stored_params.disposition;
    url_params.user_gesture = stored_params.user_gesture;
    url_params.referrer = stored_params.referrer;
    url_params.transition = stored_params.transition;
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: HandleNavigationImpl "
           "overriding url_params with stored params from CanCreateWindow";
  }
  if (pending_tracker && !url_params.frame_tree_node_id.is_null()) {
    pending_tracker->SetWebViewFrameTreeNodeId(url_params.frame_tree_node_id);
    OMNIBOX_LOG("window_tracker")
        << "Associated webview_frame_tree_node_id: "
        << url_params.frame_tree_node_id.value() << " with tracker for task: "
        << pending_tracker->task_id().value().AsLowercaseString();
  }

  // Determine if this is a navigation to a new tab. Its considered new tab
  // if the disposition will be a new tab/window, or if there is a tracked
  // window for this navigation. The tracked window covers cases that go through
  // CanCreateWindow which doesn't get intercepted, then ends up here without
  // the disposition.
  bool is_to_new_tab =
      from_can_create_window || source_contents->GetOpener() != nullptr ||
      url_params.disposition == WindowOpenDisposition::NEW_FOREGROUND_TAB ||
      url_params.disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB ||
      url_params.disposition == WindowOpenDisposition::NEW_WINDOW ||
      (pending_tracker != nullptr);
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: HandleNavigationImpl "
         "is_to_new_tab="
      << is_to_new_tab;

  // Intercept any navigation where the wrapping WebContents is the WebUI host
  // unless it is the embedded page.
  if ((is_from_embedded_page || is_forward_link_navigation) &&
      IsContextualTasksUrl(source_contents->GetLastCommittedURL())) {
    // Check if this navigation is from a <webview> that was created as part of
    // a window.open call. If so, and it has a actively open WebContents being
    // tracked, forward the navigation to that window instead. This solves the
    // flow of a user opening a window via window.open, and then changing its
    // url via window.location.href.
    if (!url_params.frame_tree_node_id.is_null()) {
      ContextualTasksWindowTracker* tracker =
          tracker_manager_
              ? tracker_manager_->FindTrackerByWebViewFrameTreeNodeId(
                    url_params.frame_tree_node_id)
              : nullptr;
      if (tracker) {
        content::WebContents* tab_contents = tracker->GetTabWebContents();
        if (tab_contents) {
          OMNIBOX_LOG("window_tracker")
              << "Redirecting webview navigation to tab. Task: "
              << tracker->task_id().value().AsLowercaseString()
              << ", URL: " << url_params.url.spec();
          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE,
              base::BindOnce(
                  [](base::WeakPtr<content::WebContents> wc,
                     content::OpenURLParams params) {
                    if (!wc) {
                      return;
                    }
                    content::NavigationController::LoadURLParams load_params(
                        params);
                    load_params.frame_tree_node_id = content::FrameTreeNodeId();
                    wc->GetController().LoadURLWithParams(load_params);
                  },
                  tab_contents->GetWeakPtr(), std::move(url_params)));
          return true;  // Cancel navigation in webview
        }
      }
    }

    // If aim is telling the browser what to do with links, the share case
    // doesn't explicitly need to be handled. If aim doesn't communicate this,
    // handle share links specifically to avoid ambiguity with other special-
    // case, host-based navigation.
    if (!base::FeatureList::IsEnabled(kAimTriggeredThreadLinks) &&
        IsShareUrl(url_params.url)) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: HandleNavigationImpl "
             "posting OpenUrl";
      // Since the web content will no longer be hosted in the side panel, make
      // sure to remove the param that makes the page render for it.
      content::OpenURLParams new_url_params = url_params;
      new_url_params.url = lens::RemoveSidePanelURLParameters(url_params.url);
      new_url_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&ContextualTasksUiService::OpenUrl,
                                    weak_ptr_factory_.GetWeakPtr(),
                                    std::move(new_url_params),
                                    blink::mojom::WindowFeatures(), browser));
      return true;
    }

    // Ignore navigation triggered by UI except forward link navigation.
    if (!(url_params.is_renderer_initiated || is_forward_link_navigation)) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: HandleNavigationImpl "
             "returning false, not renderer initiated";
      return false;
    }
    // Allow users to sign in within the <webview>.
    // TODO(crbug.com/454388385): Remove this once the authentication flow is
    // implemented.
    if (is_nav_to_sign_in) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: HandleNavigationImpl "
             "returning false, sign in domain";
      return false;
    }

    base::Uuid task_id;
    if (source_contents) {
      task_id = GetTaskIdFromUrl(source_contents->GetLastCommittedURL());
    }

    // If the navigation is to a search results page or AI page, it is allowed
    // if being viewed in the side panel, but only if it is intercepted without
    // the side panel-specific params. If the params have already been added, do
    // nothing, otherwise this logic causes an infinite "intercept" loop. Any
    // "allowed domain" (e.g. Google) should not be treated as a thread link
    // unless the link is expected to open in a new tab (e.g. the anchor tag was
    // set to target="_blank").
    bool is_allowed_host = IsAllowedHost(url_params.url) &&
                           !(is_from_embedded_page && is_to_new_tab);
    if (is_allowed_host || is_nav_to_ai) {
      if (tab) {
        if (!is_nav_to_ai) {
          OMNIBOX_LOG("nav_trace")
              << "ContextualTasks navigation trace: HandleNavigationImpl "
                 "posting OnNonThreadNavigationInTab";
          // The SRP should never be embedded in the WebUI when viewed in a tab.
          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE,
              base::BindOnce(
                  &ContextualTasksUiService::OnNonThreadNavigationInTab,
                  weak_ptr_factory_.GetWeakPtr(), std::move(url_params),
                  tab->GetWeakPtr()));
          return true;
        } else {
          OMNIBOX_LOG("nav_trace")
              << "ContextualTasks navigation trace: HandleNavigationImpl "
                 "returning false, embedded AI page";
          // Allow any navigations to an AI page from embedded page.
          return false;
        }
      } else if (IsValidSearchResultsPage(url_params.url) || is_nav_to_ai ||
                 IsGoogleCaptchaUrl(url_params.url)) {
        if (!lens::HasCommonSearchQueryParameters(url_params.url) &&
            !IsGoogleCaptchaUrl(url_params.url)) {
          OMNIBOX_LOG("nav_trace")
              << "ContextualTasks navigation trace: HandleNavigationImpl "
                 "posting OnSearchResultsNavigationInSidePanel";
          ContextualTasksUIInterface* webui_controller =
              GetWebUiInterface(source_contents);

          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE,
              base::BindOnce(&ContextualTasksUiService::
                                 OnSearchResultsNavigationInSidePanel,
                             weak_ptr_factory_.GetWeakPtr(),
                             std::move(url_params), webui_controller));
          return true;
        }

        // If the params are present and the page is "valid" (e.g. not
        // shopping and has a query), or it is a CAPTCHA challenge page, allow
        // the navigation.
        OMNIBOX_LOG("nav_trace")
            << "ContextualTasks navigation trace: HandleNavigationImpl "
               "returning false, valid SRP with params or CAPTCHA URL";
        return false;
      }
    }

    // On mobile phones without window tracking, link navigations that request
    // new window creation cannot create separate windows. Intercept them here
    // and route to `OnThreadLinkClicked` for bottom-sheet handling, whereas
    // Desktop/AL allows natural window creation and handles AIM links at line
    // 1846.
    if (IsAndroidMobileFormFactor() && from_can_create_window &&
        ShouldAllowNewTabOpen(url_params.url, browser, task_id)) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(
              &ContextualTasksUiService::OnThreadLinkClicked,
              weak_ptr_factory_.GetWeakPtr(), url_params.url, task_id,
              tab ? tab->GetWeakPtr() : nullptr,
              browser ? browser->GetWeakPtr() : nullptr,
              initiator_origin.value_or(source_contents->GetPrimaryMainFrame()
                                            ->GetLastCommittedOrigin())));
      return true;  // Return true to cancel natural (unsupported) window
                    // creation on mobile.
    }

    // If this is a navigation CanCreateWindow, check to see if this
    // navigation should open in a new tab, or needs to be cancelled. If the
    // former, return true to allow the window to be created. Afterwards, the
    // new tab will be created and the navigation throttle will be called
    // again, which will then handle the `OnThreadLinkClick` via the call
    // below. Allowing the window to open is a requirement to allow
    // `window.open` calls to receive a Window object.
    if (GetIsContextualTasksWindowTrackingEnabled() && from_can_create_window &&
        ShouldAllowNewTabOpen(url_params.url, browser, task_id)) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: HandleNavigationImpl "
             "allowing natural opening for new tab";

      if (tracker_manager_) {
        // Create a tracker to associate this navigation with the task.
        // Store the initiator's RFH ID to prevent matching different contents.
        // At this point, `source_contents` is known to be the WebUI page. So we
        // can pass `source_contents->GetWeakPtr()` as the webui_contents.
        // Additionally, initiator_frame_token is used for tracking, so if its
        // missing, fallback to the WebUI primary frame token.
        auto tracker = std::make_unique<ContextualTasksWindowTracker>(
            ContextualTaskId(task_id), url_params.url,
            initiator_frame_token.has_value()
                ? initiator_frame_token.value()
                : source_contents->GetPrimaryMainFrame()->GetGlobalFrameToken(),
            /*webui_contents=*/source_contents->GetWeakPtr(),
            base::BindOnce(&ContextualTasksUiService::RemoveWindowTracker,
                           weak_ptr_factory_.GetWeakPtr()));
        tracker->SetOpenURLParams(url_params);
        tracker->SetWindowFeatures(window_features);
        if (!url_params.frame_tree_node_id.is_null()) {
          tracker->SetWebViewFrameTreeNodeId(url_params.frame_tree_node_id);
          OMNIBOX_LOG("window_tracker")
              << "Associated webview_frame_tree_node_id: "
              << url_params.frame_tree_node_id.value()
              << " with tracker for task: "
              << tracker->task_id().value().AsLowercaseString();
        }
        OMNIBOX_LOG("window_tracker")
            << "Stored window features for URL " << url_params.url.spec()
            << ": bounds=[" << window_features.bounds.x() << ","
            << window_features.bounds.y() << " "
            << window_features.bounds.width() << "x"
            << window_features.bounds.height() << "]";
        tracker_manager_->AddTracker(std::move(tracker));
        tabs::TabInterface* source_tab =
            tabs::TabInterface::MaybeGetFromContents(source_contents);
        if (source_tab) {
          BrowserWindowInterface* current_browser =
              source_tab->GetBrowserWindowInterface();
          if (current_browser) {
            TabListInterface* tab_list =
                TabListInterface::From(current_browser);
            if (tab_list) {
              tracker_manager_->ObserveTabList(tab_list);
            }
          }
        }
      }
      return false;
    }

    std::vector<content::WebContents*> inner_contents =
        source_contents->GetInnerWebContents();
    bool is_nav_from_embedded_non_ai_page = false;
    // For this feature there should only ever be a single inner WebContents if
    // one exists at all.
    if (inner_contents.size() > 0) {
      is_nav_from_embedded_non_ai_page =
          is_from_embedded_page &&
          !IsAiUrl(inner_contents[0]->GetLastCommittedURL());
    }

    // A message about how to treat links is only received from aim pages. In
    // lens flows, link behavior is still treated as best-guess since the SRP
    // doesn't message the browser when a link is clicked - search results
    // links still need to be opened as a thread link.
    if (base::FeatureList::IsEnabled(kAimTriggeredThreadLinks) &&
        !is_nav_from_embedded_non_ai_page) {
      if (is_forward_link_navigation) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                &ContextualTasksUiService::OnThreadLinkClicked,
                weak_ptr_factory_.GetWeakPtr(), url_params.url, task_id,
                tab ? tab->GetWeakPtr() : nullptr,
                browser ? browser->GetWeakPtr() : nullptr,
                initiator_origin.value_or(source_contents->GetPrimaryMainFrame()
                                              ->GetLastCommittedOrigin())));
        return true;
      }

      if (!is_to_new_tab) {
        if (tab) {
          // Links from the embedded page that would need to navigate the tab
          // need special treatment so they navigate the tab rather than the
          // embedded page.
          OMNIBOX_LOG("nav_trace")
              << "ContextualTasks navigation trace: HandleNavigationImpl "
                 "posting OnNonThreadNavigationInTab";
          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE,
              base::BindOnce(
                  &ContextualTasksUiService::OnNonThreadNavigationInTab,
                  weak_ptr_factory_.GetWeakPtr(), std::move(url_params),
                  tab->GetWeakPtr()));
        } else {
          // In-tab links from within the side panel shouldn't navigate the side
          // panel. Some lans cases can, but that is handled earlier in this
          // method. For this case open a new tab instead.
          url_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE, base::BindOnce(&ContextualTasksUiService::OpenUrl,
                                        weak_ptr_factory_.GetWeakPtr(),
                                        std::move(url_params),
                                        pending_tracker
                                            ? pending_tracker->window_features()
                                            : blink::mojom::WindowFeatures(),
                                        browser));
        }
        return true;
      } else {
        // Allow the link to open to its intended destination as deemed by the
        // url_params.
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(&ContextualTasksUiService::OpenUrl,
                           weak_ptr_factory_.GetWeakPtr(),
                           std::move(url_params),
                           pending_tracker ? pending_tracker->window_features()
                                           : blink::mojom::WindowFeatures(),
                           browser));
        return true;
      }
    } else {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: HandleNavigationImpl "
             "posting OnThreadLinkClicked";
      // This needs to be posted in case the called method triggers a navigation
      // in the same WebContents, invalidating the nav handle used up the chain.
      url::Origin final_initiator_origin =
          initiator_origin.value_or(url_params.initiator_origin.value_or(
              url::Origin::Create(source_contents->GetLastCommittedURL())));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(
              static_cast<void (ContextualTasksUiService::*)(
                  const GURL&, base::Uuid, base::WeakPtr<tabs::TabInterface>,
                  base::WeakPtr<BrowserWindowInterface>, const url::Origin&)>(
                  &ContextualTasksUiService::OnThreadLinkClicked),
              weak_ptr_factory_.GetWeakPtr(), url_params.url, task_id,
              tab ? tab->GetWeakPtr() : nullptr,
              browser ? browser->GetWeakPtr() : nullptr,
              final_initiator_origin));
      return true;
    }
  }

  // Navigations to the AI URL in the topmost frame should always be
  // intercepted.
  if (is_nav_to_ai) {
    if ((!aim_eligibility_service_ ||
         !aim_eligibility_service_->IsCobrowseEligible()) &&
        !IsActiveTabInContext(source_contents)) {
      OMNIBOX_LOG("nav_trace")
          << "ContextualTasks navigation trace: HandleNavigationImpl "
             "returning false, nav to AI but not cobrowse eligible";
      return false;
    }

    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: HandleNavigationImpl "
           "posting OnNavigationToAiPageIntercepted";
    // This needs to be posted in case the called method triggers a navigation
    // in the same WebContents, invalidating the nav handle used up the chain.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            &ContextualTasksUiService::OnNavigationToAiPageIntercepted,
            weak_ptr_factory_.GetWeakPtr(), url_params.url,
            tab ? tab->GetWeakPtr() : nullptr, is_to_new_tab));
    return true;
  }

  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: HandleNavigationImpl "
         "returning false at end of method";
  // Allow anything else.
  return false;
}

#if !BUILDFLAG(IS_ANDROID)
void ContextualTasksUiService::OnBackButtonExpandsSidePanel(
    base::WeakPtr<tabs::TabInterface> weak_tab) {
  if (!weak_tab) {
    return;
  }
  auto* browser = weak_tab->GetBrowserWindowInterface();
  if (!browser) {
    return;
  }
  auto* controller =
      contextual_tasks::ContextualTasksPanelController::From(browser);
  if (controller && controller->IsPanelOpenForContextualTask()) {
    base::RecordAction(
        base::UserMetricsAction("ContextualTasks.BackButton.UserAction."
                                "NavigatedFromSidePanelToFullTab"));
    base::UmaHistogramBoolean(
        "ContextualTasks.BackButton.UserAction."
        "NavigatedFromSidePanelToFullTab",
        true);

    content::WebContents* side_panel_contents =
        controller->GetActiveWebContents();
    controller->MoveTaskUiToNewTab();
    auto* tab_strip_model = browser->GetTabStripModel();
    if (tab_strip_model) {
      int index = tab_strip_model->GetIndexOfTab(weak_tab.get());
      if (index != TabStripModel::kNoTab) {
        // Append last committed URL of the original tab to the
        // side panel WebContents to make the forward navigation
        // work.
        if (side_panel_contents) {
          GURL last_committed_url =
              weak_tab->GetContents()->GetLastCommittedURL();
          if (last_committed_url.is_valid() && !last_committed_url.is_empty()) {
            std::vector<std::unique_ptr<content::NavigationEntry>> entries;
            entries.push_back(
                content::NavigationController::CreateNavigationEntry(
                    last_committed_url, content::Referrer(), std::nullopt,
                    std::nullopt, ui::PAGE_TRANSITION_LINK, false,
                    std::string(), browser->GetProfile(), nullptr));
            side_panel_contents->GetController().PruneAllButLastCommitted();
            side_panel_contents->GetController().Restore(
                side_panel_contents->GetController()
                    .GetLastCommittedEntryIndex(),
                content::RestoreType::kRestored, &entries);

            // Calling Restore() marks the NavigationController with
            // NeedsReload() = true. Since side_panel_contents is already live
            // and committed, consume and stop the reload immediately so it does
            // not refresh when subsequently shown.
            if (side_panel_contents->GetController().NeedsReload()) {
              side_panel_contents->GetController().LoadIfNecessary();
              side_panel_contents->Stop();
            }
          }
        }
        tab_strip_model->CloseWebContentsAt(
            index, TabCloseTypes::CLOSE_EXPAND_SIDE_PANEL);
      }
    }
  }
}
#endif

void ContextualTasksUiService::LoadUrlInWebContents(
    const GURL& url,
    base::WeakPtr<content::WebContents> web_contents) {
  if (!web_contents) {
    return;
  }
  content::NavigationController::LoadURLParams params(url);
  params.transition_type = ::ui::PAGE_TRANSITION_AUTO_TOPLEVEL;
  web_contents->GetController().LoadURLWithParams(params);
}

void ContextualTasksUiService::ScheduleRedirectWebUIUrlToAim(
    content::OpenURLParams url_params,
    base::WeakPtr<content::WebContents> source_contents,
    tabs::TabInterface* target_tab) {
  url_params.url = GetAiUrlFromWebUIUrl(GetDefaultAiPageUrl(), url_params.url);

  // Fall back to the main active tab window contents if the raw pointer is
  // unassigned
  base::WeakPtr<content::WebContents> destination_contents = source_contents;
  if (!destination_contents && target_tab) {
    destination_contents = target_tab->GetContents()->GetWeakPtr();
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(
                     [](base::WeakPtr<content::WebContents> wc,
                        const content::OpenURLParams& open_url_params) {
                       if (!wc) {
                         return;
                       }
                       content::NavigationController::LoadURLParams load_params(
                           open_url_params);
                       load_params.transition_type =
                           ui::PAGE_TRANSITION_AUTO_TOPLEVEL;

                       // This triggers the breakout from the secure subframe
                       // cleanly
                       wc->GetController().LoadURLWithParams(load_params);
                     },
                     destination_contents, std::move(url_params)));
}

#if !BUILDFLAG(IS_ANDROID)
std::unique_ptr<lens::LensMediaLinkHandler>
ContextualTasksUiService::CreateMediaLinkHandler(
    content::WebContents* web_contents) {
  return std::make_unique<lens::LensMediaLinkHandler>(web_contents);
}
#endif

bool ContextualTasksUiService::IsUrlForPrimaryAccount(const GURL& url) {
  return contextual_tasks::IsUrlForPrimaryAccount(identity_manager_, url);
}

bool ContextualTasksUiService::IsSignedInToBrowserWithValidCredentials() {
  return contextual_tasks::IsSignedInToBrowserWithValidCredentials(
      identity_manager_);
}

bool ContextualTasksUiService::CookieJarContainsPrimaryAccount() {
  return contextual_tasks::CookieJarContainsPrimaryAccount(identity_manager_);
}

ContextualTasksEligibilityManager*
ContextualTasksUiService::GetEligibilityManager() const {
  return eligibility_manager_.get();
}

omnibox::ChromeAimEntryPoint
ContextualTasksUiService::GetInitialEntryPointForTask(
    const base::Uuid& task_id) {
  auto it = task_id_to_entry_point_override_.find(task_id);
  if (it != task_id_to_entry_point_override_.end()) {
    return it->second;
  }
  return omnibox::ChromeAimEntryPoint::UNKNOWN_AIM_ENTRY_POINT;
}
void ContextualTasksUiService::OpenFeedbackUi(BrowserWindowInterface* browser,
                                              const GURL& page_url) {
  delegate_->OpenFeedbackUi(browser, page_url);
}

void ContextualTasksUiService::ShowUndoSnackbar(
    BrowserWindowInterface* browser_window_interface) {
  delegate_->ShowUndoSnackbar(browser_window_interface);
}

void ContextualTasksUiService::StartPlatformVoiceRecognition(
    BrowserWindowInterface* browser,
    content::WebContents* requesting_contents) {
  web_contents_for_outstanding_voice_request_ =
      requesting_contents->GetWeakPtr();

  if (delegate_) {
    delegate_->StartPlatformVoiceRecognition(browser);
  }
}

void ContextualTasksUiService::OnVoiceTranscribed(const std::string& query) {
  // Take ownership and reset the pending tab tracker
  base::WeakPtr<content::WebContents> web_contents =
      std::move(web_contents_for_outstanding_voice_request_);
  if (!web_contents) {
    LOG(WARNING) << "WebContents was destroyed before voice results returned.";
    return;
  }

  content::WebUI* web_ui = web_contents->GetWebUI();
  if (!web_ui) {
    LOG(WARNING) << "WebUI is null.";
    return;
  }

  auto* controller = web_ui->GetController();
  if (!controller) {
    LOG(WARNING) << "WebUI controller is null.";
    return;
  }

  auto* contextual_tasks_ui = static_cast<ContextualTasksUI*>(controller);
  if (!contextual_tasks_ui) {
    LOG(WARNING) << "ContextualTasksUi is null.";
    return;
  }
  contextual_tasks_ui->OnVoiceTranscribed(query);
}

GURL ContextualTasksUiService::GetContextualTaskUrlForTask(
    const base::Uuid& task_id) {
  GURL url(chrome::kChromeUIContextualTasksURL);

  std::string host = GetHostForTask(task_id);
  if (!host.empty()) {
    url = net::AppendQueryParameter(url, kChromeHostParam, host);
  }

  url = net::AppendQueryParameter(url, kTaskQueryParam,
                                  task_id.AsLowercaseString());

  // Copy the 'cs' parameter, if it exists, from the task's creation URL to
  // the WebUI host URL so that the WebUI theme matches the target page theme
  // immediately.
  std::optional<GURL> creation_url = GetCreationUrlForTask(task_id);
  if (creation_url) {
    std::optional<bool> is_dark_mode =
        contextual_tasks::GetDarkModeFromUrl(*creation_url);
    if (is_dark_mode.has_value()) {
      url = net::AppendOrReplaceQueryParameter(
          url, "cs", is_dark_mode.value() ? "1" : "0");
    }
  }

  omnibox::ChromeAimEntryPoint entry_point =
      GetInitialEntryPointForTask(task_id);
  return AppendAimEntryPointParams(url, entry_point);
}

std::string ContextualTasksUiService::GetHostForTask(
    const base::Uuid& task_id) {
  auto it = task_id_to_creation_url_.find(task_id);
  if (it != task_id_to_creation_url_.end()) {
    std::optional<std::string> host = GetHostFromUrl(it->second);
    if (host.has_value()) {
      return *host;
    }

    std::string_view creation_host = it->second.host();
    GURL default_ai_url(kAiPageHost);
    std::string_view default_host = default_ai_url.host();

    std::string stripped_host =
        url_formatter::StripWWW(std::string(creation_host));
    std::string stripped_default_host =
        url_formatter::StripWWW(std::string(default_host));

    if (!stripped_host.empty() && stripped_host != stripped_default_host) {
      return std::string(creation_host);
    }
  }

  std::string forced_host = GetForcedEmbeddedPageHost();
  if (!forced_host.empty()) {
    return forced_host;
  }

  return "";
}

void ContextualTasksUiService::RemoveWindowTracker(
    base::WeakPtr<ContextualTasksWindowTracker> tracker) {
  if (!GetIsContextualTasksWindowTrackingEnabled()) {
    return;
  }
  if (!tracker) {
    return;
  }
  if (tracker->window_id().has_value()) {
    if (tracker->webui_contents()) {
      content::WebContents* webui_contents = tracker->webui_contents().get();
      auto* web_ui_interface = GetWebUiInterface(webui_contents);
      if (web_ui_interface) {
        if (web_ui_interface->GetPageRemote().is_bound()) {
          web_ui_interface->GetPageRemote()->OnWindowClosed(
              tracker->window_id().value());
        }
      }
    }
  }
  tracker_manager_->RemoveTracker(tracker.get());
}

void ContextualTasksUiService::RegisterWindow(ContextualTaskId task_id,
                                              const GURL& url,
                                              ContextualWindowId window_id) {
  if (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_) {
    tracker_manager_->RegisterWindow(task_id, url, window_id);
  }
}

void ContextualTasksUiService::CloseTrackedWindow(
    ContextualWindowId window_id) {
  if (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_) {
    tracker_manager_->CloseTrackedWindow(window_id);
  }
}

bool ContextualTasksUiService::IsTrustedHost(const std::string& host) {
  if (host.empty()) {
    return false;
  }

  // Handle localhost and loopback addresses. Note: `net::HostStringIsLocalhost`
  // does not recognize bracketed IPv6 literals like "[::1]", so we explicitly
  // check for "[::1]" in addition to standard loopback host strings.
  if (host == "localhost" || host == "127.0.0.1" || host == "[::1]" ||
      host == "::1" || net::HostStringIsLocalhost(host)) {
    return true;
  }

  url::CanonHostInfo host_info;
  std::string canonical_host = net::CanonicalizeHost(host, &host_info);
  if (canonical_host.empty() ||
      host_info.family == url::CanonHostInfo::BROKEN) {
    return false;
  }

  if (!net::IsCanonicalizedHostCompliant(canonical_host)) {
    return false;
  }

  if (net::IsSubdomainOf(canonical_host, "corp.google.com") ||
      net::IsSubdomainOf(canonical_host, "c.googlers.com") ||
      net::IsSubdomainOf(canonical_host, "proxy.googlers.com")) {
    return true;
  }

  return false;
}

std::optional<std::string> ContextualTasksUiService::GetHostFromUrl(
    const GURL& url) {
  std::string host;
  if (net::GetValueForKeyInQuery(url, kChromeHostParam, &host) &&
      IsTrustedHost(host)) {
    if (host == "[::1]" || host == "::1") {
      return "[::1]";
    }
    url::CanonHostInfo host_info;
    std::string canonical_host = net::CanonicalizeHost(host, &host_info);
    if (!canonical_host.empty() &&
        host_info.family != url::CanonHostInfo::BROKEN) {
      return canonical_host;
    }
    return host;
  }
  return std::nullopt;
}

void ContextualTasksUiService::SetInitialEntryPointForTask(
    const base::Uuid& task_id,
    omnibox::ChromeAimEntryPoint entry_point) {
  if (entry_point != omnibox::ChromeAimEntryPoint::UNKNOWN_AIM_ENTRY_POINT) {
    task_id_to_entry_point_override_[task_id] = entry_point;
  }
}

std::optional<GURL> ContextualTasksUiService::GetInitialUrlForTask(
    const base::Uuid& uuid) {
  auto it = task_id_to_creation_url_.find(uuid);
  if (it != task_id_to_creation_url_.end()) {
    GURL url = it->second;
    // Ensure the sourceid param is set. This is needed to identify chrome
    // source traffic for AIM pages that were directly navigated to by the user,
    // as opposed to threads created by Chrome.
    url = net::AppendOrReplaceQueryParameter(url, "sourceid", "chrome");
    url = net::AppendOrReplaceQueryParameter(url, "ccb", "1");
    task_id_to_creation_url_.erase(it);
    omnibox::ChromeAimEntryPoint entry_point =
        GetInitialEntryPointForTask(uuid);
    OMNIBOX_LOG("nav_trace")
        << "ContextualTasks navigation trace: GetInitialUrlForTask "
           "returning URL with entry point";
    return AppendAimEntryPointParams(url, entry_point);
  }
  OMNIBOX_LOG("nav_trace")
      << "ContextualTasks navigation trace: GetInitialUrlForTask "
         "returning nullopt";
  return std::nullopt;
}

std::optional<GURL> ContextualTasksUiService::GetCreationUrlForTask(
    const base::Uuid& task_id) {
  auto it = task_id_to_creation_url_.find(task_id);
  if (it != task_id_to_creation_url_.end()) {
    return it->second;
  }
  return std::nullopt;
}

void ContextualTasksUiService::AddPendingUrlCallback(
    const base::Uuid& task_id,
    base::OnceCallback<void(const GURL&)> callback) {
  tasks_waiting_for_url_[task_id] = std::move(callback);
}

bool ContextualTasksUiService::IsTaskWaitingForUrl(const base::Uuid& task_id) {
  return tasks_waiting_for_url_.contains(task_id);
}

void ContextualTasksUiService::GetThreadUrlFromTaskId(
    const base::Uuid& task_id,
    base::OnceCallback<void(GURL)> callback) {
  contextual_tasks_service_->GetThreadUrlFromTaskId(
      task_id, g_browser_process->GetApplicationLocale(),
      GetInitialEntryPointForTask(task_id), std::move(callback));
}

GURL ContextualTasksUiService::GetDefaultAiPageUrl() {
  return GURL(GetContextualTasksAiPageUrl());
}

GURL ContextualTasksUiService::GetDefaultAiPageUrlForTask(
    const base::Uuid& task_id) {
  GURL url = GetDefaultAiPageUrl();

  omnibox::ChromeAimEntryPoint entry_point =
      GetInitialEntryPointForTask(task_id);
  return AppendAimEntryPointParams(url, entry_point);
}

bool ContextualTasksUiService::IsTrackedWindow(
    content::WebContents* web_contents) {
  if (GetIsContextualTasksWindowTrackingEnabled() && tracker_manager_ &&
      tracker_manager_->IsTrackedWindow(web_contents)) {
    return true;
  }

  // Also considered tracked if the opener is a guest (e.g. in a webview).
  content::WebContents* opener_wc =
      web_contents->GetOpener()
          ? content::WebContents::FromRenderFrameHost(web_contents->GetOpener())
          : nullptr;
  return opener_wc && opener_wc != opener_wc->GetResponsibleWebContents() &&
         IsContextualTasksUrl(opener_wc->GetLastCommittedURL());
}

void ContextualTasksUiService::OnTaskChanged(
    BrowserWindowInterface* browser_window_interface,
    content::WebContents* web_contents,
    const std::optional<base::Uuid>& old_task_id,
    const std::optional<base::Uuid>& new_task_id,
    bool is_shown_in_tab) {
  if (!browser_window_interface) {
    return;
  }

  ContextualTasksPanelController* controller =
      ContextualTasksPanelController::From(browser_window_interface);

  if (is_shown_in_tab) {
    auto* contextual_search_service =
        ContextualSearchServiceFactory::GetForProfile(profile_.get());
    UpdateContextualSearchWebContentsHelperForTask(
        contextual_search_service, browser_window_interface,
        contextual_tasks_service_, controller, web_contents,
        new_task_id.value_or(base::Uuid()));

    auto* active_task_context_provider =
        ActiveTaskContextProvider::From(browser_window_interface);
    if (active_task_context_provider) {
      active_task_context_provider->RefreshContext();
    }
  } else {  // !is_shown_in_tab
    // If a new thread is started in the panel, affiliated tabs should change
    // their thread to the new one.
    base::Uuid final_task_id = new_task_id.value_or(base::Uuid());
    if (!final_task_id.is_valid()) {
      // If the panel is in zero state, create an empty task.
      ContextualTask task = contextual_tasks_service_->CreateTask();
      final_task_id = task.GetTaskId();
    }

    TabListInterface* tab_list =
        TabListInterface::From(browser_window_interface);
    content::WebContents* active_contents =
        tab_list->GetActiveTab()->GetContents();
    SessionID active_id = SessionTabHelper::IdForTab(active_contents);

    // If the current tab is associated with any task, change associations for
    // all tabs associated with that task.
    std::optional<ContextualTask> current_task =
        contextual_tasks_service_->GetContextualTaskForTab(active_id);
    if (current_task) {
      std::vector<SessionID> tab_ids =
          contextual_tasks_service_->GetTabsAssociatedWithTask(
              current_task->GetTaskId());
      for (const auto& id : tab_ids) {
        contextual_tasks_service_->AssociateTabWithTask(final_task_id, id);
      }
    } else {
      contextual_tasks_service_->AssociateTabWithTask(final_task_id, active_id);
    }

    controller->OnTaskChanged(web_contents, final_task_id);
  }
}

void ContextualTasksUiService::TurnOnSmartTabSharing(
    BrowserWindowInterface* browser) {
  if (!browser || browser->GetProfile() != profile_) {
    return;
  }

  // Check side panel.
  auto* controller = ContextualTasksPanelController::From(browser);
  if (controller && controller->IsPanelOpenForContextualTask()) {
    content::WebContents* web_contents = controller->GetActiveWebContents();
    if (auto* web_ui_interface = GetWebUiInterface(web_contents)) {
      if (web_ui_interface->GetPageRemote().is_bound()) {
        web_ui_interface->GetPageRemote()->TurnOnSmartTabSharing();
      }
    }
  }

  // Check all tabs.
  TabListInterface* tab_list = TabListInterface::From(browser);
  if (tab_list) {
    for (int i = 0; i < tab_list->GetTabCount(); ++i) {
      content::WebContents* web_contents = tab_list->GetTab(i)->GetContents();
      if (auto* web_ui_interface = GetWebUiInterface(web_contents)) {
        if (web_ui_interface->GetPageRemote().is_bound()) {
          web_ui_interface->GetPageRemote()->TurnOnSmartTabSharing();
        }
      }
    }
  }
}

void ContextualTasksUiService::MoveTaskUiToNewTab(
    const base::Uuid& task_id,
    BrowserWindowInterface* browser,
    const GURL& inner_frame_url) {
  auto* controller = ContextualTasksPanelController::From(browser);
  CHECK(controller);

  // If the side panel wasn't showing an AI page, don't embed the page in the
  // webui - navigate directly to the link instead.
  if (!IsAiUrl(inner_frame_url)) {
    // Since the web content will no longer be hosted in the side panel, make
    // sure to remove the param that makes the page render for it.
    NavigateParams params(browser,
                          lens::RemoveSidePanelURLParameters(inner_frame_url),
                          ui::PAGE_TRANSITION_LINK);
    params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
    Navigate(&params);

  } else {
    std::unique_ptr<content::WebContents> web_contents =
        controller->DetachWebContentsForTask(task_id);
    if (!web_contents) {
      return;
    }

    // Make sure to acquire a raw pointer handle to the WebContents prior to
    // std::moving it below since it's used later in this function.
    auto* web_contents_ptr = web_contents.get();
    NavigateParams params(browser, std::move(web_contents));
    params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
    params.transition = ui::PAGE_TRANSITION_LINK;
    Navigate(&params);

    // Notify the WebUI that the tab status has changed only after the contents
    // has been moved to a tab.
    if (auto* web_ui_interface = GetWebUiInterface(web_contents_ptr)) {
      web_ui_interface->OnSidePanelStateChanged();
    }
  }

  controller->Close();

  ContextualTasksService* task_service =
      ContextualTasksServiceFactory::GetForProfile(browser->GetProfile());
  CHECK(task_service);
  task_service->DisassociateAllTabsFromTask(task_id);
}

void ContextualTasksUiService::StartTaskUiInSidePanel(
    BrowserWindowInterface* browser_window_interface,
    tabs::TabInterface* tab_interface,
    const GURL& url,
    std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
        session_handle,
    StartTaskUiOptions options) {
  StartTaskUiInSidePanelImpl(browser_window_interface, tab_interface, url,
                             std::move(session_handle), std::move(options));
}

void ContextualTasksUiService::StartTaskUiInSidePanelImpl(
    BrowserWindowInterface* browser_window_interface,
    tabs::TabInterface* tab_interface,
    const GURL& url,
    std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
        session_handle,
    StartTaskUiOptions options) {
  CHECK(!url.is_empty());
  CHECK(contextual_tasks_service_);

  // Abort if the tab is no longer active to prevent opening the panel on the
  // wrong tab.
  if (!tab_interface || !tab_interface->IsActivated()) {
    return;
  }

  // Get the controller for the current window.
  auto* controller =
      ContextualTasksPanelController::From(browser_window_interface);
  auto* panel_contents = controller->GetActiveWebContents();

  // Create a task for the URL if the side panel wasn't already showing a task.
  if (!panel_contents || !controller->IsPanelOpenForContextualTask()) {
    base::Uuid task_id;
    if (options.use_mstk_for_task_association) {
      std::string mstk;
      if (net::GetValueForKeyInQuery(url, "mstk", &mstk)) {
        for (const auto& [id, initial_mstk] : task_id_to_initial_mstk_) {
          if (initial_mstk == mstk) {
            task_id = id;
            break;
          }
        }
      }
    }

    if (!task_id.is_valid()) {
      ContextualTask task = contextual_tasks_service_->CreateTaskFromUrl(url);
      task_id = task.GetTaskId();
      task_id_to_creation_url_[task_id] = url;
      std::string mstk;
      if (net::GetValueForKeyInQuery(url, "mstk", &mstk)) {
        task_id_to_initial_mstk_[task_id] = mstk;
      }
    } else {
      // Even if a task already exists, make sure to use the URL given to this
      // method.
      task_id_to_creation_url_[task_id] = url;
    }

    if (options.associate_web_contents) {
      AssociateWebContentsToTask(tab_interface->GetContents(), task_id);
    } else {
      // Associating the WebContents is used for two things, 1) to know which
      // task to open next to the given WebContents and 2) add the WebContents
      // as context implicitly. We don't want to do the latter, so set the
      // pending task so the former still happens.
      controller->SetPendingTaskForTab(tab_interface, task_id);
    }
    if (session_handle) {
      pending_session_handles_.emplace(task_id, std::move(session_handle));
    }
    controller->Show(/*transition_from_tab=*/false, options.entry_point,
                     options.use_no_animation, options.open_time_ticks);

    InitializeTaskInSidePanel(controller->GetActiveWebContents(), task_id,
                              nullptr);
    return;
  }

  // If the side panel contents already exist, get the WebUI controller to
  // load the URL into the already loaded contextual tasks UI.
  auto* helper = ContextualSearchWebContentsHelper::GetOrCreateForWebContents(
      panel_contents);
  // If the task was waiting for a URL to be generated (e.g. opened early
  // with ghost loader but no URL), provide the URL now to unblock the WebUI's
  // initial pull request via GetUrlForTask.
  if (helper->task_id().has_value() &&
      IsTaskWaitingForUrl(helper->task_id().value())) {
    if (IsContextualTasksSidePanelRearchitectureEnabled()) {
      LoadUrlInSidePanel(panel_contents, url);
    }
    OnInitialThreadUrlAvailable(helper->task_id().value(), url);
    return;
  }

  if (IsContextualTasksSidePanelRearchitectureEnabled()) {
    if (ShouldReloadZeroState(url, this)) {
      // TODO(crbug.com/537842795): Understand if this flow is possible in the
      // rearchitecture and handle accordingly. For now, just load the URL.
    }
    LoadUrlInSidePanel(panel_contents, url);
    return;
  }

  // Otherwise, if the panel is already open and initialized, push the
  // navigation directly to the embedded page.
  if (ContextualTasksUIInterface* web_ui_interface =
          GetWebUiInterface(panel_contents)) {
    if (ShouldReloadZeroState(url, this)) {
      // Cleanly start over: Create a new task and reload the parent WebUI.
      ContextualTask task = contextual_tasks_service_->CreateTaskFromUrl(url);
      task_id_to_creation_url_[task.GetTaskId()] = url;
      AssociateWebContentsToTask(tab_interface->GetContents(),
                                 task.GetTaskId());

      content::NavigationController::LoadURLParams load_params(
          GetContextualTaskUrlForTask(task.GetTaskId()));
      panel_contents->GetController().LoadURLWithParams(load_params);

      InitializeTaskInSidePanel(panel_contents, task.GetTaskId(),
                                std::move(session_handle));
      return;
    }

    if (IsContextualTasksSidePanelRearchitectureEnabled()) {
      panel_contents->GetController().LoadURL(url, content::Referrer(),
                                              ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
                                              std::string());
    } else {
      content::OpenURLParams url_params(
          url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB,
          ui::PAGE_TRANSITION_LINK, /*is_renderer_initiated=*/false);
      web_ui_interface->TransferNavigationToEmbeddedPage(url_params);
    }
  }
}

void ContextualTasksUiService::InitSidePanelWithGhostLoader(
    BrowserWindowInterface* browser_window_interface,
    tabs::TabInterface* tab_interface,
    std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
        session_handle,
    omnibox::ChromeAimEntryPoint entry_point) {
  CHECK(contextual_tasks_service_);

  // Get the controller for the current window.
  auto* controller =
      ContextualTasksPanelController::From(browser_window_interface);
  auto* panel_contents = controller->GetActiveWebContents();
  // If the side panel is already open for a task, do nothing. The ghost
  // loader will be shown whenever the URL navigation is transferred to the
  // embedded page.
  if (panel_contents || controller->IsPanelOpenForContextualTask()) {
    return;
  }

  // Create a task for the URL if the side panel wasn't already showing a task.
  ContextualTask task = contextual_tasks_service_->CreateTask();
  tasks_waiting_for_url_[task.GetTaskId()] = base::NullCallback();
  AssociateWebContentsToTask(tab_interface->GetContents(), task.GetTaskId());
  if (session_handle) {
    pending_session_handles_.emplace(task.GetTaskId(),
                                     std::move(session_handle));
  }
  controller->Show(/*transition_from_tab=*/false, entry_point);

  InitializeTaskInSidePanel(controller->GetActiveWebContents(),
                            task.GetTaskId(), nullptr);
}

void ContextualTasksUiService::StartTaskUiInSidePanelWithErrorPage(
    BrowserWindowInterface* browser_window_interface,
    tabs::TabInterface* tab_interface,
    std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
        session_handle,
    omnibox::ChromeAimEntryPoint entry_point) {
  // Abort if the tab is no longer active to prevent opening the panel on the
  // wrong tab.
  if (!tab_interface || !tab_interface->IsActivated()) {
    return;
  }

  // Create a new task.
  ContextualTask task = contextual_tasks_service_->CreateTask();
  auto* controller =
      ContextualTasksPanelController::From(browser_window_interface);
  auto* panel_contents = controller->GetActiveWebContents();
  auto source = session_handle && session_handle->GetMetricsRecorder()
                    ? session_handle->GetMetricsRecorder()->source()
                    : contextual_search::ContextualSearchSource::kUnknown;

  if (tab_interface) {
    AssociateWebContentsToTask(tab_interface->GetContents(), task.GetTaskId());
  }

  bool panel_was_closed =
      !panel_contents || !controller->IsPanelOpenForContextualTask();
  if (panel_was_closed) {
    pending_error_page_tasks_.emplace(task.GetTaskId(), source);
    if (session_handle) {
      pending_session_handles_.emplace(task.GetTaskId(),
                                       std::move(session_handle));
    }
    controller->Show(/*transition_from_tab=*/false, entry_point);
  }

  content::WebContents* web_contents = controller->GetActiveWebContents();
  InitializeTaskInSidePanel(
      web_contents, task.GetTaskId(),
      panel_was_closed ? nullptr : std::move(session_handle));

  if (!panel_was_closed) {
    if (auto* web_ui_interface = GetWebUiInterface(web_contents)) {
      ShowAndRecordErrorPage(web_ui_interface->GetPageRemote(), source);
    }
  }
}

bool ContextualTasksUiService::IsAiUrl(const GURL& url) {
  if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) {
    return false;
  }

  // TODO(crbug.com/543997783): Have the PEC API return this as an AIM URL
  // instead of hardcoding it here.
  if (url.host() == "g.ai" || url.host() == "www.g.ai") {
    return true;
  }
  return aim_eligibility_service_ &&
         aim_eligibility_service_->IsAimUrl(url, GetForcedEmbeddedPageHost());
}

bool ContextualTasksUiService::IsSidePanelOpenAndRequestInSidePanel(
    content::WebContents* web_contents) {
  if (!web_contents) {
    return false;
  }
  BrowserWindowInterface* browser =
      webui::GetBrowserWindowInterface(web_contents);
  if (!browser) {
    return false;
  }
  auto* controller = ContextualTasksPanelController::From(browser);
  if (!controller || !controller->IsPanelOpenForContextualTask()) {
    return false;
  }
  return std::ranges::contains(controller->GetPanelWebContentsList(),
                               web_contents);
}

bool ContextualTasksUiService::IsPendingErrorPage(const base::Uuid& task_id) {
  if (!pending_error_page_tasks_.contains(task_id)) {
    return false;
  }
  RecordErrorPageShown(pending_error_page_tasks_[task_id]);
  return true;
}

bool ContextualTasksUiService::IsContextualTasksUrl(const GURL& url) {
  return contextual_tasks::IsContextualTasksUrl(url);
}

base::Uuid ContextualTasksUiService::GetTaskIdFromUrl(const GURL& url) {
  std::string task_id;
  net::GetValueForKeyInQuery(url, kTaskQueryParam, &task_id);
  return base::Uuid::ParseLowercase(task_id);
}

bool ContextualTasksUiService::IsContextualTasksDisplayUrl(const GURL& url) {
  return url.scheme() == GetContextualTasksDisplayUrlScheme() &&
         url.host() == GetContextualTasksDisplayUrlHost() &&
         url.path() == GetContextualTasksDisplayUrlPath();
}

bool ContextualTasksUiService::IsSearchResultsUrl(const GURL& url) {
  if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS() || !IsAllowedHost(url)) {
    return false;
  }

  if (!base::StartsWith(url.path(), "/search")) {
    return false;
  }

  return true;
}

bool ContextualTasksUiService::IsShareUrl(const GURL& url) {
  return url.query().find("https%3A%2F%2Fshare.google%2Faimode") !=
         std::string::npos;
}

bool ContextualTasksUiService::IsValidSearchResultsPage(const GURL& url) {
  if (!IsSearchResultsUrl(url)) {
    return false;
  }

  // Do not allow shopping mode queries.
  std::string value;
  if (net::GetValueForKeyInQuery(url, "udm", &value) && value == "28") {
    return false;
  }

  // The search results page is only valid if it has a text query or is a Lens
  // query.
  return (net::GetValueForKeyInQuery(url, kSearchQueryKey, &value) &&
          !value.empty()) ||
         (net::GetValueForKeyInQuery(url, kLensModeKey, &value) &&
          !value.empty());
}

bool ContextualTasksUiService::IsGoogleCaptchaUrl(const GURL& url) {
  if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS() || !IsAllowedHost(url)) {
    return false;
  }

  return base::StartsWith(url.path(), "/sorry/");
}

GURL ContextualTasksUiService::CopyParamsFromWebUIUrl(const GURL& base_url,
                                                      const GURL& webui_url) {
  std::optional<std::string> host_value = GetHostFromUrl(webui_url);
  GURL aim_url(base_url);

  // Extract host if present in WebUI URL and prepend it to make it
  // first.
  if (host_value.has_value()) {
    GURL::Replacements replacements;
    std::string new_query = base::StrCat({kChromeHostParam, "=", *host_value});
    replacements.SetQueryStr(new_query);
    aim_url = base_url.ReplaceComponents(replacements);

    // The QueryIterator correctly iterates over duplicate keys, and
    // GetUnescapedValue preserves their values. This ensures that duplicate
    // parameters on base_url are not lost during the transfer.
    net::QueryIterator base_it(base_url);
    while (!base_it.IsAtEnd()) {
      std::string key(base_it.GetKey());
      if (key != kChromeHostParam) {
        aim_url = net::AppendQueryParameter(aim_url, key,
                                            base_it.GetUnescapedValue());
      }
      base_it.Advance();
    }
  }
  // Now add all other params from the WebUI URL.
  net::QueryIterator it(webui_url);
  while (!it.IsAtEnd()) {
    std::string key(it.GetKey());
    if (key != kTaskQueryParam && key != kChromeHostParam) {
      std::string value;
      net::GetValueForKeyInQuery(webui_url, key, &value);
      aim_url = net::AppendOrReplaceQueryParameter(aim_url, key, value);
    }
    it.Advance();
  }

  if (webui_url.has_ref()) {
    aim_url = net::AppendOrReplaceRef(aim_url, webui_url.ref());
  } else {
    GURL::Replacements replacements;
    replacements.ClearRef();
    aim_url = aim_url.ReplaceComponents(replacements);
  }

  return aim_url;
}

GURL ContextualTasksUiService::GetAiUrlFromWebUIUrl(const GURL& base_url,
                                                    const GURL& webui_url) {
  GURL url = CopyParamsFromWebUIUrl(base_url, webui_url);

  std::optional<std::string> host_value = GetHostFromUrl(url);
  if (host_value.has_value()) {
    GURL::Replacements replacements;
    replacements.SetHostStr(*host_value);
    url = url.ReplaceComponents(replacements);
  }

  // Remove kChromeHostParam from the new url if it exists.
  return net::AppendOrReplaceQueryParameter(url, kChromeHostParam,
                                            std::nullopt);
}

void ContextualTasksUiService::OnLensOverlayStateChanged(
    BrowserWindowInterface* browser_window_interface,
    bool is_showing,
    std::optional<lens::LensOverlayInvocationSource> invocation_source) {
  auto* controller =
      ContextualTasksPanelController::From(browser_window_interface);
  if (!controller || !controller->IsPanelOpenForContextualTask()) {
    return;
  }

  auto* panel_contents = controller->GetActiveWebContents();
  if (!panel_contents) {
    return;
  }

  auto* web_ui_interface = GetWebUiInterface(panel_contents);
  if (web_ui_interface) {
    web_ui_interface->OnLensOverlayStateChanged(is_showing, invocation_source);
  }
}

void ContextualTasksUiService::AssociateWebContentsToTask(
    content::WebContents* web_contents,
    const base::Uuid& task_id) {
  SessionID session_id = SessionTabHelper::IdForTab(web_contents);
  if (session_id.is_valid()) {
    contextual_tasks_service_->AssociateTabWithTask(task_id, session_id);
  }
}

content::RenderFrameHost* ContextualTasksUiService::GetGuestForMessage(
    content::RenderFrameHost* target_rfh,
    const url::Origin& source_origin) {
  if (!GetIsContextualTasksWindowTrackingEnabled() || !tracker_manager_) {
    return nullptr;
  }
  // 1. Verify that the target of postMessage is one of our message proxy web
  // contents.
  content::WebContents* target_contents =
      content::WebContents::FromRenderFrameHost(target_rfh);
  if (!GuestOpenerUserData::IsGuestOpener(target_contents)) {
    OMNIBOX_LOG("route_message")
        << "GetGuestForMessage: target is not a message proxy";
    return nullptr;
  }

  // 2. Origin check: Only allow trusted Google origins to send messages.
  if (!IsAllowedOriginForGuestMessage(source_origin)) {
    OMNIBOX_LOG("route_message") << "GetGuestForMessage: origin not allowed "
                                 << source_origin.Serialize();
    return nullptr;
  }

  // 3. Find the tracker that manages the sender's tab.
  ContextualTasksWindowTracker* tracker =
      tracker_manager_->FindTrackerByMessageProxy(target_contents);
  if (!tracker) {
    OMNIBOX_LOG("route_message")
        << "GetGuestForMessage: no tracker found for message proxy";
    return nullptr;
  }

  // 4. Return the main frame that opened the frame this postMessage is coming
  // from. The postMessage will then be routed to this frame. This is most
  // likely the <webview>, but could also be an <iframe> within the <webview>.
  content::RenderFrameHost* initiator_frame = tracker->GetInitiatorFrame();
  if (initiator_frame) {
    OMNIBOX_LOG("route_message")
        << "GetGuestForMessage: found initiator frame. Routing there.";
    return initiator_frame;
  }

  OMNIBOX_LOG("route_message")
      << "GetGuestForMessage: returning nullptr at end";
  return nullptr;
}

const std::vector<std::unique_ptr<ContextualTasksWindowTracker>>&
ContextualTasksUiService::window_trackers_for_testing() const {
  return tracker_manager_->window_trackers_for_testing();  // IN-TEST
}

void ContextualTasksUiService::OnTabClickedFromSourcesMenu(
    int32_t tab_id,
    const GURL& url,
    BrowserWindowInterface* browser) {
  if (!browser) {
    return;
  }

  if (!url.SchemeIsHTTPOrHTTPS()) {
    // TODO(crbug.com/460614856): Handle PDF and other possible contexts.
    return;
  }

  // Find the tab on the tab strip by the given tab ID. If found, switch to it.
  // Chances are that the tab might have navigated by now, hence check the URL
  // as well.
  TabListInterface* tab_list = TabListInterface::From(browser);
  for (int i = 0; i < tab_list->GetTabCount(); ++i) {
    content::WebContents* web_contents = tab_list->GetTab(i)->GetContents();
    tabs::TabInterface* tab_interface =
        tabs::TabInterface::GetFromContents(web_contents);
    if (tab_interface && tab_interface->GetHandle().raw_value() == tab_id &&
        web_contents->GetLastCommittedURL() == url) {
      tab_list->ActivateTab(tab_interface->GetHandle());
      return;
    }
  }

  // The tab with the given ID and URL wasn't found. Next, try finding a tab
  // that matches the URL. If found, switch to it.
  for (int i = 0; i < tab_list->GetTabCount(); ++i) {
    tabs::TabInterface* tab_interface = tab_list->GetTab(i);
    if (tab_interface->GetContents()->GetLastCommittedURL() == url) {
      tab_list->ActivateTab(tab_interface->GetHandle());
      return;
    }
  }

  // The tab wasn't found. Open a new tab with the given URL to the end of the
  // tab strip.
  NavigateParams params(browser->GetProfile(), url, ui::PAGE_TRANSITION_LINK);
  params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
  Navigate(&params);
}

void ContextualTasksUiService::OnFileClickedFromSourcesMenu(
    const GURL& url,
    BrowserWindowInterface* browser) {
  if (!browser) {
    return;
  }

  if (!url.SchemeIsHTTPOrHTTPS()) {
    return;
  }

  NavigateParams params(browser->GetProfile(), url, ui::PAGE_TRANSITION_LINK);
  params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
  Navigate(&params);
}

void ContextualTasksUiService::OnImageClickedFromSourcesMenu(
    const GURL& url,
    BrowserWindowInterface* browser) {
  if (!browser) {
    return;
  }

  if (!url.SchemeIsHTTPOrHTTPS()) {
    return;
  }

  NavigateParams params(browser->GetProfile(), url, ui::PAGE_TRANSITION_LINK);
  params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
  Navigate(&params);
}

bool ContextualTasksUiService::IsAllowedHost(const GURL& url) {
  return aim_eligibility_service_ &&
         aim_eligibility_service_->IsAimHost(url, GetForcedEmbeddedPageHost());
}

void ContextualTasksUiService::OnInitialThreadUrlAvailable(
    const base::Uuid& task_id,
    const GURL& url) {
  task_id_to_creation_url_[task_id] = url;
  auto it = tasks_waiting_for_url_.find(task_id);
  if (it != tasks_waiting_for_url_.end()) {
    if (it->second) {
      std::move(it->second).Run(GetInitialUrlForTask(task_id).value());
    }
    tasks_waiting_for_url_.erase(it);
  }
}

}  // namespace contextual_tasks
