// 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/win/installer_downloader/installer_downloader_controller.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/base_paths.h"
#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/legacy_language_tag_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/uuid.h"
#include "base/version_info/channel.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/infobars/browser_infobar_manager.h"
#include "chrome/browser/infobars/infobar_features.h"
#include "chrome/browser/infobars/infobar_spec.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_constants.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_infobar_window_active_tab_tracker.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_model.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_model_impl.h"
#include "chrome/browser/win/installer_downloader/system_info_provider_impl.h"
#include "chrome/common/channel_info.h"
#include "chrome/grit/branded_strings.h"
#include "components/application_locale_storage/application_locale_storage.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/confirm_infobar_delegate.h"
#include "components/infobars/core/infobar.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/tabs/public/tab_interface.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/base_window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "url/gurl.h"

namespace installer_downloader {

namespace {

constexpr const char kUrlPrefix[] = "https://dl.google.com/tag/s/appguid%3D%7B";
constexpr const char kUrlSuffix[] =
    "%7D%26iid%3D%7BIIDGUID%7D%26lang%3DLANGUAGE%26browser%3D4%26usagestats%"
    "3DSTATS%26appname%3DGoogle%2520Chrome%26needsadmin%3Dprefers%26ap%"
    "3Dx64-statsdef_1%26brand%3DLMFN%26installdataindex%3Dempty/update2/"
    "installers/ChromeSetup.exe";

constexpr const char kStableGuid[] = "8A69D345-D564-463C-AFF1-A69D9E530F96";
constexpr const char kBetaGuid[] = "8237E44A-0054-442C-B6B6-EA0509993955";
constexpr const char kDevGuid[] = "401C381F-E0DE-4B85-8BD8-3F3F14FBDA57";
constexpr const char kCanaryGuid[] = "4EA16AC7-FD5A-47C3-875B-DBF4A2008C20";

std::string MakeInstallerTemplate(const std::string& appGuid) {
  return base::StrCat({kUrlPrefix, appGuid, kUrlSuffix});
}

std::string GetDefaultInstallerDownloadUrlTemplate() {
  switch (chrome::GetChannel()) {
    case version_info::Channel::CANARY:
      return MakeInstallerTemplate(kCanaryGuid);
    case version_info::Channel::DEV:
      return MakeInstallerTemplate(kDevGuid);
    case version_info::Channel::BETA:
      return MakeInstallerTemplate(kBetaGuid);
    default:
      return MakeInstallerTemplate(kStableGuid);
  }
  NOTREACHED();
}

std::optional<GURL> BuildInstallerDownloadUrl(bool is_metrics_enabled) {
  std::string installer_url_template = GetDefaultInstallerDownloadUrlTemplate();

  base::ReplaceFirstSubstringAfterOffset(
      &installer_url_template, /*start_offset=*/0, "IIDGUID",
      base::Uuid::GenerateRandomV4().AsLowercaseString());

  base::ReplaceFirstSubstringAfterOffset(&installer_url_template,
                                         /*start_offset=*/0, "STATS",
                                         is_metrics_enabled ? "1" : "0");

  std::string language_code = base::i18n::GetLanguageSubtagUsingLanguageTag(
      g_browser_process->GetFeatures()->application_locale_storage()->Get());
  CHECK(!language_code.empty());

  base::ReplaceFirstSubstringAfterOffset(
      &installer_url_template, /*start_offset=*/0, "LANGUAGE", language_code);

  GURL installer_url(installer_url_template);

  return installer_url.is_valid()
             ? std::optional<GURL>(std::move(installer_url))
             : std::nullopt;
}

}  // namespace

InstallerDownloaderController::InstallerDownloaderController(
    ShowInfobarCallback show_infobar_callback,
    base::RepeatingCallback<bool()> is_metrics_enabled_callback)
    : is_metrics_enabled_callback_(std::move(is_metrics_enabled_callback)),
      show_infobar_callback_(std::move(show_infobar_callback)),
      model_(std::make_unique<InstallerDownloaderModelImpl>(
          std::make_unique<SystemInfoProviderImpl>())),
      get_active_web_contents_callback_(base::BindRepeating(
          &InstallerDownloaderController::GetActiveWebContents,
          base::Unretained(this))),
      should_show_infobar_for_profile_callback_(base::BindRepeating(
          &InstallerDownloaderController::ShouldShowInfobarForCurrentProfile,
          base::Unretained(this))) {
  RegisterInfoBar();
  RegisterBrowserWindowEvents();
}

InstallerDownloaderController::InstallerDownloaderController(
    ShowInfobarCallback show_infobar_callback,
    base::RepeatingCallback<bool()> is_metrics_enabled_callback,
    std::unique_ptr<InstallerDownloaderModel> model)
    : is_metrics_enabled_callback_(std::move(is_metrics_enabled_callback)),
      show_infobar_callback_(std::move(show_infobar_callback)),
      model_(std::move(model)),
      get_active_web_contents_callback_(base::BindRepeating(
          &InstallerDownloaderController::GetActiveWebContents,
          base::Unretained(this))),
      should_show_infobar_for_profile_callback_(base::BindRepeating(
          &InstallerDownloaderController::ShouldShowInfobarForCurrentProfile,
          base::Unretained(this))) {
  RegisterInfoBar();
  RegisterBrowserWindowEvents();
}

void InstallerDownloaderController::RegisterInfoBar() {
  if (!infobars::IsInfoBarMigrated(
          infobars::InfoBarDelegate::INSTALLER_DOWNLOADER_INFOBAR_DELEGATE)) {
    return;
  }

  auto* browser_infobar_manager =
      infobars::BrowserInfoBarManager::From(g_browser_process);
  if (!browser_infobar_manager) {
    return;
  }

  auto spec =
      infobars::InfoBarSpec::Builder(
          infobars::InfoBarDelegate::INSTALLER_DOWNLOADER_INFOBAR_DELEGATE)
          .SetMessageText(
              l10n_util::GetStringUTF16(IDS_INSTALLER_DOWNLOADER_DISCLAIMER))
          .SetLinkText(l10n_util::GetStringUTF16(IDS_INSTALLER_DOWNLOADER_LINK))
          .SetLinkNavigationUrl(GURL(kLearnMoreUrl))
          .SetIcon(features::IsRoundedIconsEnabled()
                       ? omnibox::kChromeProductIcon
                       : vector_icons::kProductRefreshIcon)
          .SetScope(infobars::InfoBarScope::kGlobal)
          // The infobar should not be shown on guest profiles.
          .SetBrowserFilter(
              base::BindRepeating([](BrowserWindowInterface* browser) {
                return !browser->GetProfile()->IsGuestSession();
              }))
          .SetExpireOnNavigation(false)
          // InstallerDownloaderController is registered as a global feature and
          // this controller is a global feature outliving any infobar
          // instances, so base::Unretained(this) is safe.
          .AddOkButton(
              l10n_util::GetStringUTF16(IDS_INSTALLER_DOWNLOADER_BUTTON_LABEL),
              base::BindRepeating(
                  [](InstallerDownloaderController* controller,
                     content::WebContents*) {
                    CHECK(controller->destination_.has_value());
                    controller->OnDownloadRequestAccepted(
                        controller->destination_.value());
                  },
                  base::Unretained(this)))
          .SetDismissAction(base::BindRepeating(
              [](InstallerDownloaderController* controller,
                 content::WebContents*) { controller->OnInfoBarDismissed(); },
              base::Unretained(this)))
          .Build();

  browser_infobar_manager->Register(std::move(spec));
}

void InstallerDownloaderController::RegisterBrowserWindowEvents() {
  active_window_subscription_ =
      window_tracker_.RegisterActiveWindowChangedCallback(base::BindRepeating(
          &InstallerDownloaderController::OnActiveBrowserWindowChanged,
          base::Unretained(this)));

  if (!infobars::IsInfoBarMigrated(
          infobars::InfoBarDelegate::INSTALLER_DOWNLOADER_INFOBAR_DELEGATE)) {
    removed_window_subscription_ =
        window_tracker_.RegisterRemovedWindowCallback(base::BindRepeating(
            &InstallerDownloaderController::OnRemovedBrowserWindow,
            base::Unretained(this)));
  }
}

content::WebContents* InstallerDownloaderController::GetActiveWebContents() {
  BrowserWindowInterface* last_active_window =
      window_tracker_.get_last_active_window();
  if (!last_active_window) {
    return nullptr;
  }

  tabs::TabInterface* active_tab = last_active_window->GetActiveTabInterface();
  if (!active_tab) {
    return nullptr;
  }

  content::WebContents* web_contents = active_tab->GetContents();
  if (!web_contents || web_contents->IsBeingDestroyed()) {
    return nullptr;
  }
  return web_contents;
}

InstallerDownloaderController::~InstallerDownloaderController() = default;

void InstallerDownloaderController::OnActiveBrowserWindowChanged(
    BrowserWindowInterface* bwi) {
  // This can be null during  the startup or when the last window is closed.
  if (!bwi) {
    return;
  }

  if (infobars::IsInfoBarMigrated(
          infobars::InfoBarDelegate::INSTALLER_DOWNLOADER_INFOBAR_DELEGATE)) {
    MaybeShowInfoBar();
    return;
  }

  if (bwi_and_active_tab_tracker_map_.contains(bwi)) {
    return;
  }

  bwi_and_active_tab_tracker_map_[bwi] =
      std::make_unique<InstallerDownloaderInfobarWindowActiveTabTracker>(
          bwi,
          base::BindRepeating(&InstallerDownloaderController::MaybeShowInfoBar,
                              base::Unretained(this)));
}

void InstallerDownloaderController::OnRemovedBrowserWindow(
    BrowserWindowInterface* bwi) {
  if (!bwi_and_active_tab_tracker_map_.contains(bwi)) {
    return;
  }

  bwi_and_active_tab_tracker_map_.erase(bwi);
}

bool InstallerDownloaderController::ShouldShowInfobarForCurrentProfile() {
  // The infobar should not be shown on guest profiles.
  BrowserWindowInterface* last_active_window =
      window_tracker_.get_last_active_window();
  if (!last_active_window ||
      last_active_window->GetProfile()->IsGuestSession()) {
    return false;
  }

  return true;
}

void InstallerDownloaderController::MaybeShowInfoBar() {
  // The max show count of the infobar have been reached. Eligibility check is
  // no longer needed.
  if (!model_->CanShowInfobar()) {
    return;
  }

  if (!should_show_infobar_for_profile_callback_.Run()) {
    return;
  }

  model_->CheckEligibility(
      base::BindOnce(&InstallerDownloaderController::OnEligibilityReady,
                     base::Unretained(this)));
}

void InstallerDownloaderController::OnEligibilityReady(
    std::optional<base::FilePath> destination) {
  if (infobar_closed_) {
    return;
  }

  // Early return when we have no destination and bypass is not allowed.
  if (!destination.has_value() && !model_->ShouldByPassEligibilityCheck()) {
    return;
  }

  if (infobars::IsInfoBarMigrated(
          infobars::InfoBarDelegate::INSTALLER_DOWNLOADER_INFOBAR_DELEGATE)) {
    if (infobar_shown_) {
      return;
    }

    // Compute a fallback destination (user’s Desktop) when bypassing
    // eligibility.
    if (!destination) {
      base::FilePath desktop_path;
      if (!base::PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) {
        return;
      }

      destination = std::move(desktop_path);
    }

    destination_ = destination;

    auto* browser_infobar_manager =
        infobars::BrowserInfoBarManager::From(g_browser_process);
    CHECK(browser_infobar_manager);

    browser_infobar_manager->ShowGlobally(
        infobars::InfoBarDelegate::INSTALLER_DOWNLOADER_INFOBAR_DELEGATE);

    infobar_shown_ = true;
    model_->IncrementShowCount();
    base::UmaHistogramBoolean("Windows.InstallerDownloader.InfobarShown",
                              /*sample=*/true);
    // Log cycle index with exclusive max of 8 to future-proof against
    // increases in max cycle count. Current max is 3.
    base::UmaHistogramExactLinear(
        "Windows.InstallerDownloader.Reengagement.InfobarShown",
        model_->GetCurrentCycle(), 8);
    return;
  }

  auto* contents = get_active_web_contents_callback_.Run();

  if (!contents) {
    return;
  }

  if (visible_infobars_web_contents_.contains(contents)) {
    return;
  }

  // Compute a fallback destination (user’s Desktop) when bypassing eligibility.
  if (!destination) {
    base::FilePath desktop_path;
    if (!base::PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) {
      return;
    }

    destination = std::move(desktop_path);
  }

  infobars::ContentInfoBarManager* infobar_manager =
      infobars::ContentInfoBarManager::FromWebContents(contents);

  // Installer Downloader is a global feature, therefore it's guaranteed that
  // InstallerDownloaderController will be alive at any point during the browser
  // runtime.
  infobars::InfoBar* infobar = show_infobar_callback_.Run(
      infobar_manager,
      base::BindOnce(&InstallerDownloaderController::OnDownloadRequestAccepted,
                     base::Unretained(this), destination.value()),
      base::BindOnce(&InstallerDownloaderController::OnInfoBarDismissed,
                     base::Unretained(this)));

  if (!infobar) {
    return;
  }

  if (infobar_manager) {
    infobar_manager->AddObserver(this);
  } else {
    CHECK_IS_TEST();
  }

  visible_infobars_web_contents_[contents] = infobar;

  // This is the first show in this browser session.
  if (visible_infobars_web_contents_.size() == 1u) {
    model_->IncrementShowCount();
    base::UmaHistogramBoolean("Windows.InstallerDownloader.InfobarShown",
                              /*sample=*/true);
    // Log cycle index with exclusive max of 8 to future-proof against
    // increases in max cycle count. Current max is 3.
    base::UmaHistogramExactLinear(
        "Windows.InstallerDownloader.Reengagement.InfobarShown",
        model_->GetCurrentCycle(), 8);
  }
}

void InstallerDownloaderController::OnInfoBarRemoved(infobars::InfoBar* infobar,
                                                     bool animate) {
  auto it = std::find_if(
      visible_infobars_web_contents_.begin(),
      visible_infobars_web_contents_.end(),
      [infobar](const auto& entry) { return entry.second == infobar; });

  if (it == visible_infobars_web_contents_.end()) {
    return;
  }

  it->second->owner()->RemoveObserver(this);
  visible_infobars_web_contents_.erase(it);

  if (!user_initiated_info_bar_close_pending_) {
    return;
  }

  for (auto [contents, infobar_instance] : visible_infobars_web_contents_) {
    infobar_instance->owner()->RemoveObserver(this);
    infobar_instance->RemoveSelf();
  }

  visible_infobars_web_contents_.clear();
  infobar_closed_ = true;
}

void InstallerDownloaderController::OnDownloadRequestAccepted(
    const base::FilePath& destination) {
  base::UmaHistogramBoolean("Windows.InstallerDownloader.RequestAccepted",
                            true);
  // Log cycle index with exclusive max of 8 to future-proof against
  // increases in max cycle count. Current max is 3.
  base::UmaHistogramExactLinear(
      "Windows.InstallerDownloader.Reengagement.RequestAccepted",
      model_->GetCurrentCycle(), 8);

  user_initiated_info_bar_close_pending_ = true;
  infobar_closed_ = true;
  // User have explicitly gave download consent. Therefore, a background
  // download should be issued.
  auto* contents = get_active_web_contents_callback_.Run();

  if (!contents) {
    return;
  }

  std::optional<GURL> installer_url =
      BuildInstallerDownloadUrl(is_metrics_enabled_callback_.Run());

  if (!installer_url.has_value()) {
    return;
  }

  // Keep the profile alive until the download completes.
  auto* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
  auto keep_alive = std::make_unique<ScopedProfileKeepAlive>(
      profile, ProfileKeepAliveOrigin::kDownloadInProgress);

  model_->StartDownload(
      installer_url.value(),
      destination.AppendASCII(kDownloadedInstallerFileName),
      CHECK_DEREF(profile->GetDownloadManager()),
      base::BindOnce(&InstallerDownloaderController::OnDownloadCompleted,
                     base::Unretained(this), std::move(keep_alive)));
}

void InstallerDownloaderController::OnDownloadCompleted(
    std::unique_ptr<ScopedProfileKeepAlive> keep_alive,
    bool success) {
  base::UmaHistogramBoolean("Windows.InstallerDownloader.DownloadSucceed",
                            success);
  model_->PreventFutureDisplay();
  if (success) {
    model_->RecordDownloadCompleted();
  }
}

void InstallerDownloaderController::SetActiveWebContentsCallbackForTesting(
    GetActiveWebContentsCallback callback) {
  get_active_web_contents_callback_ = std::move(callback);
}

void InstallerDownloaderController::
    SetShouldShowInfobarForProfileCallbackForTesting(
        ShouldShowInfobarForProfileCallback callback) {
  should_show_infobar_for_profile_callback_ = std::move(callback);
}

void InstallerDownloaderController::OnInfoBarDismissed() {
  base::UmaHistogramBoolean("Windows.InstallerDownloader.RequestAccepted",
                            false);
  user_initiated_info_bar_close_pending_ = true;
  infobar_closed_ = true;
  model_->PreventFutureDisplay();
}

}  // namespace installer_downloader
