// Copyright 2020 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/views/hats/hats_next_web_dialog.h"

#include "base/feature_list.h"
#include "base/json/json_writer.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_destroyer.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/dialogs/browser_dialogs.h"
#include "chrome/browser/ui/hats/hats_service.h"
#include "chrome/browser/ui/hats/hats_service_desktop.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/survey_config.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/toolbar/app_menu_control.h"
#include "chrome/common/chrome_features.h"
#include "components/constrained_window/constrained_window_views.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_ui.h"
#include "net/base/url_util.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/webview/web_dialog_view.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/web_dialogs/web_dialog_delegate.h"

constexpr gfx::Size HatsNextWebDialog::kMinSize;
constexpr gfx::Size HatsNextWebDialog::kMaxSize;
constexpr char kSurveyQuestionAnsweredRegex[] = "answer-(\\d+)-((?:\\d+,?)+)";
constexpr char kSurveyQuestionAnsweredAnswerRegex[] = "(\\d+),?";
constexpr char kHatsSurveyCompletedHistogram[] =
    "Feedback.HappinessTrackingSurvey.SurveyCompleted";

void LogUmaHistogramSparse(
    const std::optional<std::string>& hats_histogram_name,
    int enumeration) {
  if (hats_histogram_name.has_value()) {
    base::UmaHistogramSparse(hats_histogram_name.value(), enumeration);
  }
}

void LogUmaHistogramSparse(
    const std::optional<std::string>& hats_histogram_name,
    HatsNextWebDialog::SurveyHistogramEnumeration enumeration) {
  return LogUmaHistogramSparse(hats_histogram_name,
                               static_cast<int>(enumeration));
}

views::BubbleAnchor GetBubbleAnchor(BrowserWindowInterface* browser) {
  if (browser->GetType() == BrowserWindowInterface::Type::TYPE_DEVTOOLS) {
    return views::BubbleAnchor(
        BrowserView::GetBrowserViewForBrowser(browser)->top_container());
  }
  auto* control = BrowserView::GetBrowserViewForBrowser(browser)
                      ->toolbar_button_provider()
                      ->GetAppMenuControl();
  return control ? control->GetAnchor() : views::BubbleAnchor();
}

// WebView which contains the WebContents displaying the HaTS Next survey.
class HatsNextWebDialog::HatsWebView : public views::WebView {
  METADATA_HEADER(HatsWebView, views::WebView)

 public:
  HatsWebView(content::BrowserContext* browser_context,
              BrowserWindowInterface* browser,
              HatsNextWebDialog* dialog)
      : views::WebView(browser_context), dialog_(dialog), browser_(browser) {}

  ~HatsWebView() override = default;

  // content::WebContentsDelegate:
  bool HandleContextMenu(content::RenderFrameHost& render_frame_host,
                         const content::ContextMenuParams& params) override {
    // Ignores context menu.
    return true;
  }
  bool IsWebContentsCreationOverridden(
      content::RenderFrameHost* opener,
      content::SiteInstance* source_site_instance,
      content::mojom::WindowContainerType window_container_type,
      const GURL& opener_url,
      const std::string& frame_name,
      const GURL& target_url) override {
    return true;
  }
  content::WebContents* CreateCustomWebContents(
      content::RenderFrameHost* opener,
      content::SiteInstance* source_site_instance,
      bool is_new_browsing_instance,
      const GURL& opener_url,
      const std::string& frame_name,
      const GURL& target_url,
      WindowOpenDisposition disposition,
      const blink::mojom::WindowFeatures& window_features,
      const content::StoragePartitionConfig& partition_config,
      content::SessionStorageNamespace* session_storage_namespace) override {
    // The HaTS Next WebDialog runs with a non-primary OTR profile. This profile
    // cannot open new browser windows, so they are instead opened in the
    // regular browser that initiated the HaTS survey.
    content::OpenURLParams params =
        content::OpenURLParams(target_url, content::Referrer(),
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               ui::PAGE_TRANSITION_LINK, false);

    // For the case where we are showing a survey in an undocked DevTools
    // window, we open the URL in the browser of the inspected page.
    if (browser_->GetType() == BrowserWindowInterface::Type::TYPE_DEVTOOLS) {
      DevToolsWindow* devtools_window =
          DevToolsWindow::AsDevToolsWindow(browser_);
      DCHECK(devtools_window);
      devtools_window->OpenURLFromInspectedTab(params);
    } else {
      browser_->OpenURL(params, /*navigation_handle_callback=*/{});
    }
    return nullptr;
  }

  // TODO(crbug.com/40285934): Remove this whole function after HaTSWebUI is
  // launched.
  // content::WebContentsObserver:
  void DidStartNavigation(
      content::NavigationHandle* navigation_handle) override {
    if (navigation_handle->IsSameDocument() &&
        navigation_handle->IsRendererInitiated()) {
      dialog_->OnSurveyStateUpdateReceived(
          navigation_handle->GetURL().GetRef());
    }
  }

 private:
  raw_ptr<HatsNextWebDialog> dialog_;
  raw_ptr<BrowserWindowInterface> browser_;
};

BEGIN_METADATA(HatsNextWebDialog, HatsWebView)
END_METADATA

HatsNextWebDialog::HatsNextWebDialog(
    BrowserWindowInterface* browser,
    const std::string& trigger_id,
    const std::optional<std::string>& hats_histogram_name,
    const std::optional<uint64_t> hats_survey_ukm_id,
    base::OnceClosure success_callback,
    base::OnceClosure failure_callback,
    const SurveyBitsData& product_specific_bits_data,
    const SurveyStringData& product_specific_string_data)
    : HatsNextWebDialog(
          browser,
          trigger_id,
          hats_histogram_name,
          hats_survey_ukm_id,
          GURL(features::kHappinessTrackingSurveysHostedUrl.Get()),
          base::Seconds(10),
          std::move(success_callback),
          std::move(failure_callback),
          product_specific_bits_data,
          product_specific_string_data) {}

gfx::Size HatsNextWebDialog::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  gfx::Size preferred_size =
      views::View::CalculatePreferredSize(available_size);
  preferred_size.SetToMax(kMinSize);
  preferred_size.SetToMin(kMaxSize);
  return preferred_size;
}

void HatsNextWebDialog::OnProfileWillBeDestroyed(Profile* profile) {
  DCHECK_EQ(profile, otr_profile_);
  otr_profile_ = nullptr;
}

std::optional<std::string> HatsNextWebDialog::GetHistogramName() {
  return hats_histogram_name_;
}

void HatsNextWebDialog::OnSurveyLoaded() {
  // If this has already been called, do nothing. This makes the code robust,
  // should it be called multiple times.
  if (received_survey_loaded_) {
    return;
  }
  loading_timer_.Stop();
  // Record that the survey was shown, and display the widget.
  auto* service =
      HatsServiceFactory::GetForProfile(browser_->GetProfile(), false);
  DCHECK(service);
  service->RecordSurveyAsShown(trigger_id_);
  received_survey_loaded_ = true;
  ShowWidget();
  LogUmaHistogramSparse(hats_histogram_name_,
                        SurveyHistogramEnumeration::kSurveyLoadedEnumeration);
  if (hats_survey_ukm_id_.has_value()) {
    ukm_hats_builder_.SetSurveyLoaded(true);
  }
  std::move(success_callback_).Run();
}

void HatsNextWebDialog::OnSurveyCompleted() {
  base::UmaHistogramBoolean(kHatsSurveyCompletedHistogram, true);
  LogUmaHistogramSparse(
      hats_histogram_name_,
      SurveyHistogramEnumeration::kSurveyCompletedEnumeration);
  if (hats_survey_ukm_id_.has_value()) {
    ukm_hats_builder_.SetSurveyCompleted(true);
  }
}

void HatsNextWebDialog::OnSurveyClosed() {
  loading_timer_.Stop();
  if (!received_survey_loaded_) {
    // Receiving a close state prior to a loaded state indicates that contact
    // was made with the HaTS Next service, but the HaTS service declined the
    // survey request. This is likely because of a survey misconfiguration,
    // such as a survey still being in test mode, or an invalid survey ID.
    base::UmaHistogramEnumeration(
        kHatsShouldShowSurveyReasonHistogram,
        HatsServiceDesktop::ShouldShowSurveyReasons::kNoRejectedByHatsService);
    std::move(failure_callback_).Run();
  }
  if (hats_survey_ukm_id_.has_value()) {
    ukm_hats_builder_.Record(ukm::UkmRecorder::Get());
  }
  CloseWidget();
}

void HatsNextWebDialog::OnSurveyQuestionAnswered(const std::string& state) {
  if (!hats_histogram_name_.has_value() && !hats_survey_ukm_id_.has_value()) {
    return;
  }

  int question;
  std::vector<int> question_answers;
  if (!ParseSurveyQuestionAnswer(state, &question, &question_answers)) {
    LogUmaHistogramSparse(
        hats_histogram_name_,
        SurveyHistogramEnumeration::kSurveyQuestionAnswerParseError);
    return;
  }

  if (hats_survey_ukm_id_.has_value()) {
    uint64_t ukm_value = EncodeUkmQuestionAnswers(question_answers);

    switch (question) {
      case 1:
        ukm_hats_builder_.SetSurveyAnswerToQuestion1(ukm_value);
        break;
      case 2:
        ukm_hats_builder_.SetSurveyAnswerToQuestion2(ukm_value);
        break;
      case 3:
        ukm_hats_builder_.SetSurveyAnswerToQuestion3(ukm_value);
        break;
      case 4:
        ukm_hats_builder_.SetSurveyAnswerToQuestion4(ukm_value);
        break;
      case 5:
        ukm_hats_builder_.SetSurveyAnswerToQuestion5(ukm_value);
        break;
      case 6:
        ukm_hats_builder_.SetSurveyAnswerToQuestion6(ukm_value);
        break;
      case 7:
        ukm_hats_builder_.SetSurveyAnswerToQuestion7(ukm_value);
        break;
      case 8:
        ukm_hats_builder_.SetSurveyAnswerToQuestion8(ukm_value);
        break;
      case 9:
        ukm_hats_builder_.SetSurveyAnswerToQuestion9(ukm_value);
        break;
    }
  }

  if (hats_histogram_name_.has_value()) {
    for (int answer : question_answers) {
      LogUmaHistogramSparse(hats_histogram_name_,
                            GetHistogramBucket(question, answer));
    }
  }
}

// static
bool HatsNextWebDialog::ParseSurveyQuestionAnswer(const std::string& input,
                                                  int* question,
                                                  std::vector<int>* answers) {
  std::string question_num_string;
  re2::StringPiece all_answers_string;
  if (!RE2::FullMatch(input, kSurveyQuestionAnsweredRegex, &question_num_string,
                      &all_answers_string)) {
    return false;
  }

  if (!base::StringToInt(question_num_string, question) || *question <= 0 ||
      *question > 10) {
    return false;
  }

  std::string answer_string;
  while (RE2::FindAndConsume(&all_answers_string,
                             kSurveyQuestionAnsweredAnswerRegex,
                             &answer_string)) {
    int answer;
    if (!base::StringToInt(answer_string, &answer) || answer <= 0 ||
        answer > 100) {
      return false;
    }
    answers->push_back(answer);
  }

  return true;
}

// static
uint64_t HatsNextWebDialog::EncodeUkmQuestionAnswers(
    const std::vector<int>& question_answers) {
  uint64_t ukm_value = 0;
  for (int answer : question_answers) {
    if (answer > 0) {
      ukm_value |= 1 << (answer - 1);
    }
  }
  return ukm_value;
}

HatsNextWebDialog::HatsNextWebDialog(
    BrowserWindowInterface* browser,
    const std::string& trigger_id,
    const std::optional<std::string>& hats_histogram_name,
    const std::optional<uint64_t> hats_survey_ukm_id,
    const GURL& hats_survey_url,
    const base::TimeDelta& timeout,
    base::OnceClosure success_callback,
    base::OnceClosure failure_callback,
    const SurveyBitsData& product_specific_bits_data,
    const SurveyStringData& product_specific_string_data)
    : BubbleDialogDelegateView(GetBubbleAnchor(browser),
                               views::BubbleBorder::TOP_RIGHT,
                               views::BubbleBorder::DIALOG_SHADOW,
                               /*autosize=*/true),
      otr_profile_(browser->GetProfile()->GetOffTheRecordProfile(
          Profile::OTRProfileID::CreateUnique("HaTSNext:WebDialog"),
          /*create_if_needed=*/true)),
      browser_(browser),
      browser_close_subscription_(browser->RegisterBrowserDidClose(
          base::BindRepeating(&HatsNextWebDialog::BrowserDidClose,
                              base::Unretained(this)))),
      trigger_id_(trigger_id),
      hats_histogram_name_(
          hats::SurveyConfig::ValidateHatsHistogramName(hats_histogram_name)),
      hats_survey_ukm_id_(
          hats::SurveyConfig::ValidateHatsSurveyUkmId(hats_survey_ukm_id)),
      hats_survey_url_(hats_survey_url),
      timeout_(timeout),
      success_callback_(std::move(success_callback)),
      failure_callback_(std::move(failure_callback)),
      product_specific_bits_data_(product_specific_bits_data),
      product_specific_string_data_(product_specific_string_data),
      ukm_hats_builder_(browser->GetTabStripModel()
                            ->GetActiveWebContents()
                            ->GetPrimaryMainFrame()
                            ->GetPageUkmSourceId()) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  otr_profile_->AddObserver(this);
  set_close_on_deactivate(false);

  // Override the default zoom level for ths HaTS dialog. Its size should align
  // with native UI elements, rather than web content.
  content::HostZoomMap::GetDefaultForBrowserContext(otr_profile_)
      ->SetZoomLevelForHost(hats_survey_url_.GetHost(),
                            blink::ZoomFactorToZoomLevel(1.0f));

  SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));

  SetLayoutManager(std::make_unique<views::FillLayout>());
  web_view_ =
      AddChildView(std::make_unique<HatsWebView>(otr_profile_, browser, this));
  web_view_->LoadInitialURL(GetParameterizedHatsURL());
  web_view_->EnableSizingFromWebContents(kMinSize, kMaxSize);

  SetProperty(views::kElementIdentifierKey, kHatsNextWebDialogId);

  set_margins(gfx::Insets());
  widget_ = views::BubbleDialogDelegateView::CreateBubble(this);

  if (hats_survey_ukm_id_.has_value()) {
    ukm_hats_builder_.SetSurveyId(hats_survey_ukm_id_.value());
    ukm_hats_builder_.SetSurveyLoaded(false);
    ukm_hats_builder_.SetSurveyCompleted(false);
  }

  loading_timer_.Start(FROM_HERE, timeout_,
                       base::BindOnce(&HatsNextWebDialog::LoadTimedOut,
                                      weak_factory_.GetWeakPtr()));
}

HatsNextWebDialog::~HatsNextWebDialog() {
#if IS_ANDROID
  NOTIMPLEMENTED();  // This class is for desktop only. Enforce assumption.
#endif
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (otr_profile_) {
    otr_profile_->RemoveObserver(this);
    ProfileDestroyer::DestroyOTRProfileWhenAppropriate(otr_profile_);
  }
  HatsServiceDesktop* service = static_cast<HatsServiceDesktop*>(
      HatsServiceFactory::GetForProfile(browser_->GetProfile(), false));
  DCHECK(service);
  service->HatsNextDialogClosed();

  // Explicitly clear the delegate to ensure it is not invalid between now and
  // when the web contents is destroyed in the base class.
  web_view_->web_contents()->SetDelegate(nullptr);
}

// TODO(crbug.com/40285934): Remove this whole function after HaTSWebUI is
// launched.
GURL HatsNextWebDialog::GetParameterizedHatsURL() const {
  GURL param_url =
      net::AppendQueryParameter(hats_survey_url_, "trigger_id", trigger_id_);

  // Append any Product Specific Data to the query. This will be interpreted
  // by the wrapper website and provided to the HaTS backend service.
  base::DictValue dict;
  for (const auto& field_value : product_specific_bits_data_) {
    dict.Set(field_value.first, base::ToString(field_value.second));
  }
  for (const auto& field_value : product_specific_string_data_) {
    dict.Set(field_value.first, field_value.second);
  }

  std::string product_specific_data_json = base::WriteJson(dict).value_or("");

  param_url = net::AppendQueryParameter(param_url, "product_specific_data",
                                        product_specific_data_json);

  // The HaTS backend service accepts a list of preferred languages, although
  // only the application locale is provided here to ensure that the survey
  // matches the native UI language.
  base::ListValue language_list;
  language_list.Append(g_browser_process->GetApplicationLocale());

  std::string language_list_json = base::WriteJson(language_list).value_or("");
  param_url =
      net::AppendQueryParameter(param_url, "languages", language_list_json);

  if (base::FeatureList::IsEnabled(
          features::kHappinessTrackingSurveysForDesktopDemo)) {
    param_url = net::AppendQueryParameter(param_url, "enable_testing", "true");
  }

  return param_url;
}

void HatsNextWebDialog::LoadTimedOut() {
  load_timed_out_ = true;
  base::UmaHistogramEnumeration(
      kHatsShouldShowSurveyReasonHistogram,
      HatsServiceDesktop::ShouldShowSurveyReasons::kNoSurveyUnreachable);
  CloseWidget();
  std::move(failure_callback_).Run();
}

// TODO(crbug.com/40285934): Remove this whole function after HaTSWebUI is
// launched.
void HatsNextWebDialog::OnSurveyStateUpdateReceived(std::string state) {
  if (load_timed_out_) {
    // Ignore state update, since we already consider the survey load to be
    // timed out, and treated it accordingly.
    return;
  }
  loading_timer_.Stop();

  if (state == "loaded") {
    OnSurveyLoaded();
  } else if (state == "close") {
    OnSurveyClosed();
  } else if (state == "completed") {
    OnSurveyCompleted();
  } else if (base::StartsWith(state, "answer-")) {
    OnSurveyQuestionAnswered(state);
  } else {
    LOG(ERROR) << "Unknown state provided in URL fragment by HaTS survey:"
               << state;
    CloseWidget();
    LogUmaHistogramSparse(hats_histogram_name_,
                          SurveyHistogramEnumeration::kSurveyUnknownState);
    std::move(failure_callback_).Run();
  }
}

void HatsNextWebDialog::SetHatsSurveyURLforTesting(GURL url) {
  hats_survey_url_ = url;
}

void HatsNextWebDialog::ShowWidget() {
  widget_->Show();
}

void HatsNextWebDialog::CloseWidget() {
  widget_->Close();
}

bool HatsNextWebDialog::IsWaitingForSurveyForTesting() {
  return loading_timer_.IsRunning();
}

int HatsNextWebDialog::GetHistogramBucket(int question, int answer) {
  // The enumeration is specified as `QQNN`, where `QQ` is the question
  // number and `NN` is the answer index. Therefore, we can calculate this
  // value via `QQ * 100 + NN`.
  // Note: The `ParseSurveyQuestionAnswer` function guarantees that the answer
  // will be in the range [1, 100].
  // The results returned from this function should be consistent with the enum,
  // HappinessTrackingSurvey, which is defined in the file
  // tools/metrics/histograms/metadata/others/enums.xml.
  return question * 100 + answer;
}

void HatsNextWebDialog::BrowserDidClose(BrowserWindowInterface* browser) {
  widget_->CloseNow();
}

BEGIN_METADATA(HatsNextWebDialog)
ADD_READONLY_PROPERTY_METADATA(GURL, ParameterizedHatsURL)
END_METADATA
