// 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/ui/webui/new_tab_page/action_chips/action_chips_handler.h"

#include <optional>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/contextual_search/contextual_search_web_contents_helper.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/webui/new_tab_page/action_chips/action_chips.mojom.h"
#include "chrome/browser/ui/webui/new_tab_page/action_chips/action_chips_generator.h"
#include "chrome/browser/ui/webui/new_tab_page/action_chips/action_chips_metrics.h"
#include "chrome/browser/ui/webui/new_tab_page/action_chips/tab_id_generator.h"
#include "chrome/common/pref_names.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/feature_engagement/public/feature_constants.h"
#include "components/google/core/common/google_util.h"
#include "components/lens/lens_overlay_invocation_source.h"
#include "components/search/ntp_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/tabs/public/tab_interface.h"
#include "content/public/browser/clipboard_types.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/omnibox_proto/chrome_aim_entry_point.pb.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/base/window_open_disposition_utils.h"
#include "url/gurl.h"
#include "url/mojom/url.mojom.h"

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

namespace {
using ::action_chips::RecordActionChipsRetrievalLatencyMetrics;
using ::action_chips::RecordImpressionMetrics;
using ::action_chips::mojom::ActionChip;
using ::action_chips::mojom::ActionChipPtr;
using ::action_chips::mojom::TabInfo;
using ::action_chips::mojom::TabInfoPtr;
using ::tabs::TabInterface;

/**
 * Helper method to check if a tab is invalid for the Recent Tab Action Chip:
 * - Google Search Results Page (SRP) or AIM SRP
 * - Invalid URL
 * - About Blank
 * - Chrome internal page
 * - Chrome untrusted internal page
 */
#if !BUILDFLAG(IS_ANDROID)
bool IsInvalidMostRecentTab(content::WebContents& contents) {
  const GURL& url = contents.GetLastCommittedURL();
  return google_util::IsGoogleSearchUrl(url) || !url.is_valid() ||
         url.IsAboutBlank() || url.SchemeIs(content::kChromeUIScheme) ||
         url.SchemeIs(content::kChromeUIUntrustedScheme);
}

TabInterface* FindMostRecentTab(content::WebUI& web_ui) {
  auto* browser_window_interface =
      webui::GetBrowserWindowInterface(web_ui.GetWebContents());
  if (!browser_window_interface) {
    return nullptr;
  }

  auto* tab_strip_model = browser_window_interface->GetTabStripModel();
  TabInterface* most_recent_tab = nullptr;
  base::Time latest_active_time;

  for (int i = 0; i < tab_strip_model->count(); ++i) {
    TabInterface* tab = tab_strip_model->GetTabAtIndex(i);
    if (tab == most_recent_tab) {
      continue;
    }
    // TabInterface::GetContents returns a non-nullptr according to its comment.
    content::WebContents& contents = *tab->GetContents();
    if (IsInvalidMostRecentTab(contents)) {
      continue;
    }
    const base::Time last_active = contents.GetLastActiveTime();
    if (last_active > latest_active_time) {
      latest_active_time = last_active;
      most_recent_tab = tab;
    }
  }

  return most_recent_tab;
}

bool IsTabReadyForActionChipsRetrieval(content::WebContents* web_contents,
                                       const TabStripModelChange& change) {
  if (web_contents == nullptr) {
    return false;
  }

  if (change.type() == TabStripModelChange::kReplaced &&
      change.GetReplace()->old_contents == web_contents) {
    return false;
  }

  return tabs::TabInterface::GetFromContents(web_contents)->IsActivated();
}
#endif  // !BUILDFLAG(IS_ANDROID)
}  // namespace

ActionChipsHandler::ActionChipsHandler(
    mojo::PendingReceiver<action_chips::mojom::ActionChipsHandler> receiver,
    mojo::PendingRemote<action_chips::mojom::Page> page,
    Profile* profile,
    content::WebUI* web_ui,
    std::unique_ptr<ActionChipsGenerator> action_chips_generator,
    GetSessionHandleCallback get_session_handle_callback)
    : receiver_(this, std::move(receiver)),
      page_(std::move(page)),
      profile_(profile),
      web_ui_(web_ui),
      action_chips_generator_(std::move(action_chips_generator)),
      get_session_handle_callback_(std::move(get_session_handle_callback)) {
#if !BUILDFLAG(IS_ANDROID)
  content::WebContents* web_contents = web_ui_->GetWebContents();
  auto* browser_window_interface =
      webui::GetBrowserWindowInterface(web_contents);
  if (browser_window_interface) {
    // No need to call RemoveObserver later since TabStripModelObserver takes
    // care of it in its destructor.
    browser_window_interface->GetTabStripModel()->AddObserver(this);
  }
#endif  // !BUILDFLAG(IS_ANDROID)
  pref_change_registrar_.Init(profile_->GetPrefs());
  pref_change_registrar_.Add(
      prefs::kNtpToolChipsVisible,
      base::BindRepeating(&ActionChipsHandler::OnVisibilityChanged,
                          weak_factory_.GetWeakPtr()));
}

ActionChipsHandler::~ActionChipsHandler() = default;

void ActionChipsHandler::StartActionChipsRetrieval() {
  const auto start_time = base::TimeTicks::Now();
  if (!page_.is_bound()) {
    return;
  }

  TabInterface* tab = nullptr;
#if !BUILDFLAG(IS_ANDROID)
  if (contextual_search::ContextualSearchService::IsContextSharingEnabled(
          profile_->GetPrefs())) {
    tab = FindMostRecentTab(*web_ui_);
  }
#endif  // !BUILDFLAG(IS_ANDROID)

  const GURL current_url =
      tab != nullptr ? tab->GetContents()->GetLastCommittedURL() : GURL();
  if (ShouldThrottleRetrieval(current_url)) {
    return;
  }

  action_chips_generator_->GenerateActionChips(
      std::move(tab),
      base::BindOnce(&ActionChipsHandler::SendActionChipsToUi,
                     weak_factory_.GetWeakPtr(), std::move(start_time)));
}

void ActionChipsHandler::ActivateMetricsFunnel(const std::string& funnel_name) {
  auto* session_handle = get_session_handle_callback_
                             ? get_session_handle_callback_.Run()
                             : nullptr;
  if (!session_handle) {
    return;
  }

  auto* metrics_recorder = session_handle->GetMetricsRecorder();
  if (metrics_recorder) {
    metrics_recorder->ActivateMetricsFunnel(funnel_name);
  }
}

void ActionChipsHandler::SetActionChipsVisibility(bool is_visible) {
  profile_->GetPrefs()->SetBoolean(prefs::kNtpToolChipsVisible, is_visible);
}


void ActionChipsHandler::SendActionChipsToUi(base::TimeTicks start_time,
                                             std::vector<ActionChipPtr> chips) {
  if (!page_.is_bound()) {
    return;
  }
  if (chips.size() <= 1) {
    // We show a chip only when there are more than one chip. This occurs when
    // there is no tab opened and only one of the AIM features are enabled.
    // This branch ensures that no chip is displayed by returning an empty list.
    chips.clear();
  }

  RecordActionChipsRetrievalLatencyMetrics(base::TimeTicks::Now() - start_time);
  RecordImpressionMetrics(chips);
  if (!has_recorded_any_shown_) {
    action_chips::RecordActionChipsAnyShown(!chips.empty());
    has_recorded_any_shown_ = true;
  }

  page_->OnActionChipsChanged(std::move(chips));
}

#if !BUILDFLAG(IS_ANDROID)
void ActionChipsHandler::OnTabStripModelChanged(
    TabStripModel*,
    const TabStripModelChange& change,
    const TabStripSelectionChange&) {
  if (!IsTabReadyForActionChipsRetrieval(web_ui_->GetWebContents(), change)) {
    return;
  }
  StartActionChipsRetrieval();
}
#endif  // !BUILDFLAG(IS_ANDROID)

bool ActionChipsHandler::ShouldThrottleRetrieval(const GURL& current_url) {
  if (last_processed_url_ == current_url) {
    return true;
  }
  last_processed_url_ = current_url;
  return false;
}

void ActionChipsHandler::OnVisibilityChanged() {
  if (profile_->GetPrefs()->GetBoolean(prefs::kNtpToolChipsVisible)) {
    last_processed_url_.reset();
    StartActionChipsRetrieval();
  }
}

void ActionChipsHandler::NavigateToAim(
    const std::string& query_text,
    uint8_t mouse_button,
    searchbox::mojom::ActionModifiersPtr modifiers) {
  TemplateURLService* template_url_service =
      TemplateURLServiceFactory::GetForProfile(profile_);
  if (!template_url_service ||
      !template_url_service->GetDefaultSearchProvider()) {
    return;
  }

  GURL aim_url = GetUrlForAim(
      template_url_service,
      // TODO(crbug.com/540050449): This is a temporary placeholder that needs
      // to be updated to the new Action Chips entry point.
      omnibox::ChromeAimEntryPoint::DESKTOP_CHROME_NTP_REALBOX_ENTRY_POINT,
      base::Time::Now(), base::UTF8ToUTF16(query_text),
      lens::LensOverlayInvocationSource::kNtpActionChips,
      /*additional_params=*/{});

  const WindowOpenDisposition disposition = ui::DispositionFromClick(
      /*middle_button=*/mouse_button == 1, modifiers->alt_key,
      modifiers->ctrl_key, modifiers->meta_key, modifiers->shift_key);

  content::OpenURLParams params(aim_url, content::Referrer(), disposition,
                                ui::PAGE_TRANSITION_GENERATED,
                                /*is_renderer_initiated=*/false);
  web_ui_->GetWebContents()->OpenURL(params,
                                     /*navigation_handle_callback=*/{});
}
