// 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_model_impl.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "base/check.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/win/cloud_synced_folder_checker.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_feature.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_pref_names.h"
#include "chrome/browser/win/installer_downloader/system_info_provider.h"
#include "chrome/browser/win/installer_downloader/system_info_provider_impl.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/download_url_parameters.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/download_manager.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "url/gurl.h"

namespace installer_downloader {

namespace {

// Indicates that the download reason why a download failed.
//
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. If entries are added, kMaxValue
// should be updated. Keep in sync with the
// InstallerDownloaderFailureReason UMA enum defined in
// //tools/metrics/histograms/metadata/windows/enums.xml.
//
// LINT.IfChange(InstallerDownloaderFailureReason)
enum class InstallerDownloaderFailureReason {
  kInterrupted = 0,
  kCancelled = 1,
  kDestroyed = 2,

  kMaxValue = kDestroyed,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/windows/enums.xml:InstallerDownloaderFailureReason)

void RecordDownloadFailureReason(InstallerDownloaderFailureReason reason) {
  base::UmaHistogramEnumeration("Windows.InstallerDownloader.FailureReason",
                                reason);
}

void RecordDownloadInterruptReason(
    download::DownloadInterruptReason interrupt_reason) {
  // Records the interrupt reason to a sparse histogram. This is preferred for
  // enums with many possible values, where not all may be emitted.
  base::UmaHistogramSparse("Windows.InstallerDownloader.InterruptReason",
                           static_cast<int>(interrupt_reason));
}

}  // namespace

class InstallerDownloaderObserver final
    : public download::DownloadItem::Observer {
 public:
  InstallerDownloaderObserver(download::DownloadItem* item,
                              base::FilePath expected_path,
                              CompletionCallback completion_callback)
      : completion_callback_(std::move(completion_callback)),
        expected_path_(expected_path) {
    observation_.Observe(item);
  }

  InstallerDownloaderObserver(const InstallerDownloaderObserver&) = delete;
  InstallerDownloaderObserver& operator=(const InstallerDownloaderObserver&) =
      delete;

 private:
  void OnDownloadUpdated(download::DownloadItem* item) override {
    CHECK_EQ(observation_.GetSource(), item);

    const auto state = item->GetState();
    switch (state) {
      case download::DownloadItem::COMPLETE:
        // Did DownloadManager keep exactly the path we requested?
        base::UmaHistogramBoolean(
            "Windows.InstallerDownloader.DestinationMatches",
            item->GetTargetFilePath() == expected_path_);

        // `this` is deleted by `completion_callback_`, so nothing below this
        // point may access it.
        std::move(completion_callback_).Run(/*succeeded=*/true);
        break;
      case download::DownloadItem::IN_PROGRESS:
        break;
      case download::DownloadItem::INTERRUPTED:
        RecordDownloadFailureReason(
            InstallerDownloaderFailureReason::kInterrupted);
        RecordDownloadInterruptReason(item->GetLastReason());
        // `this` is deleted by `completion_callback_`, so nothing below this
        // point may access it.
        std::move(completion_callback_).Run(/*succeeded=*/false);
        break;
      case download::DownloadItem::CANCELLED:
        RecordDownloadFailureReason(
            InstallerDownloaderFailureReason::kCancelled);
        // `this` is deleted by `completion_callback_`, so nothing below this
        // point may access it.
        std::move(completion_callback_).Run(/*succeeded=*/false);
        break;
      case download::DownloadItem::MAX_DOWNLOAD_STATE:
        NOTREACHED();
    }
  }

  void OnDownloadDestroyed(download::DownloadItem* item) override {
    CHECK_EQ(observation_.GetSource(), item);
    RecordDownloadFailureReason(InstallerDownloaderFailureReason::kDestroyed);
    std::move(completion_callback_).Run(/*succeeded=*/false);
  }

  base::ScopedObservation<download::DownloadItem,
                          download::DownloadItem::Observer>
      observation_{this};
  CompletionCallback completion_callback_;

  // Stores the expected destination path of the downloaded file.
  const base::FilePath expected_path_;
};

InstallerDownloaderModelImpl::InstallerDownloaderModelImpl(
    std::unique_ptr<SystemInfoProvider> system_info_provider)
    : system_info_provider_(std::move(system_info_provider)) {}

InstallerDownloaderModelImpl::~InstallerDownloaderModelImpl() = default;

void InstallerDownloaderModelImpl::CheckEligibility(
    EligibilityCheckCallback callback) {
  if (!system_info_provider_->IsOsEligible()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
    return;
  }

  // Installer Downloader is a global feature, therefore it's guaranteed that
  // InstallerDownloaderModelImpl will be alive at any point during the browser
  // runtime.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&InstallerDownloaderModelImpl::GetInstallerDestination,
                     base::Unretained(this)),
      std::move(callback));
}

void InstallerDownloaderModelImpl::StartDownload(
    const GURL& url,
    const base::FilePath& destination,
    content::DownloadManager& download_manager,
    CompletionCallback completion_callback) {
  CHECK(url.is_valid());

  static constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
      net::DefineNetworkTrafficAnnotation("windows_installer_downloader",
                                          R"(semantics {
          sender: "Windows Installer Downloader"
          description:
            "Download Chrome installer to the user's OneDrive folder on "
            "their consent."
          trigger: "Once, when the user accepts the download request."
          data: "None."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
              owners: "//chrome/browser/win/installer_downloader/OWNERS"
            }
          }
          user_data {
            type: NONE
          }
          last_reviewed: "2026-02-10"
      }
      policy {
        cookies_allowed: NO
        setting:
          "Users can control this feature by closing the Installer "
          "Downloader Infobar."
        policy_exception_justification: "Not implemented."
    })");

  auto params = std::make_unique<download::DownloadUrlParameters>(
      url, kTrafficAnnotation);
  params->set_file_path(destination);
  params->set_transient(true);
  params->set_download_source(download::DownloadSource::INTERNAL_API);

  // The InstallerDownloaderController that hold this model is a browser global
  // feature. Therefore, it is safe to use base::Unretained here.
  params->set_callback(base::BindOnce(
      &InstallerDownloaderModelImpl::OnInstallerDownloadCreated,
      base::Unretained(this), destination, std::move(completion_callback)));

  download_manager.DownloadUrl(std::move(params));
}

bool InstallerDownloaderModelImpl::CanShowInfobar() const {
  const PrefService* local_state = g_browser_process->local_state();
  if (local_state->GetBoolean(prefs::kInstallerDownloaderDownloadCompleted)) {
    return false;
  }

  // 1. If the current cycle is still in progress, the infobar can be shown.
  if (!IsCurrentCycleFinished()) {
    return true;
  }

  // 2. If re-engagement is disabled, finish after one cycle.
  if (!base::FeatureList::IsEnabled(kInstallerDownloaderReengagement)) {
    return false;
  }

  // 3. Stop once all allowed campaign cycles are completed.
  if (GetCurrentCycle() >= installer_downloader::kMaxCycleCount.Get()) {
    return false;
  }

  // 4. Check if the cooldown period between cycles has elapsed.
  const base::Time last_show_time =
      local_state->GetTime(prefs::kInstallerDownloaderInfobarLastShowTime);
  return base::Time::Now() - last_show_time >=
         base::Days(installer_downloader::kReengagementCooldownDays.Get());
}

void InstallerDownloaderModelImpl::IncrementShowCount() {
  PrefService* local_state = g_browser_process->local_state();
  const bool cycle_finished = IsCurrentCycleFinished();

  if (base::FeatureList::IsEnabled(kInstallerDownloaderReengagement) &&
      cycle_finished) {
    // Starting a new cycle: advance cycle count, reset show count and dismissal
    // flag.
    local_state->SetInteger(prefs::kInstallerDownloaderCycleCount,
                            GetCurrentCycle() + 1);
    local_state->SetInteger(prefs::kInstallerDownloaderInfobarShowCount, 1);
    local_state->SetBoolean(prefs::kInstallerDownloaderPreventFutureDisplay,
                            false);
  } else {
    // Advancing within the current cycle.
    local_state->SetInteger(prefs::kInstallerDownloaderCycleCount,
                            GetCurrentCycle());
    local_state->SetInteger(
        prefs::kInstallerDownloaderInfobarShowCount,
        local_state->GetInteger(prefs::kInstallerDownloaderInfobarShowCount) +
            1);
  }

  local_state->SetInteger(
      prefs::kInstallerDownloaderTotalShowCount,
      local_state->GetInteger(prefs::kInstallerDownloaderTotalShowCount) + 1);

  local_state->SetTime(prefs::kInstallerDownloaderInfobarLastShowTime,
                       base::Time::Now());

  if (IsCurrentCycleFinished()) {
    if (!base::FeatureList::IsEnabled(kInstallerDownloaderReengagement) ||
        GetCurrentCycle() >= installer_downloader::kMaxCycleCount.Get()) {
      MaybeRecordTotalShowCountVal();
    }
  }
}

void InstallerDownloaderModelImpl::PreventFutureDisplay() {
  g_browser_process->local_state()->SetBoolean(
      prefs::kInstallerDownloaderPreventFutureDisplay, true);
  if (!base::FeatureList::IsEnabled(kInstallerDownloaderReengagement) ||
      GetCurrentCycle() >= installer_downloader::kMaxCycleCount.Get()) {
    MaybeRecordTotalShowCountVal();
  }
}

void InstallerDownloaderModelImpl::RecordDownloadCompleted() {
  g_browser_process->local_state()->SetBoolean(
      prefs::kInstallerDownloaderDownloadCompleted, true);
  MaybeRecordTotalShowCountVal();
}

bool InstallerDownloaderModelImpl::ShouldByPassEligibilityCheck() const {
  return g_browser_process->local_state()->GetBoolean(
      prefs::kInstallerDownloaderBypassEligibilityCheck);
}

void InstallerDownloaderModelImpl::MaybeRecordTotalShowCountVal() {
  if (total_show_count_recorded_) {
    return;
  }
  total_show_count_recorded_ = true;
  const PrefService* local_state = g_browser_process->local_state();
  // Logging with an exclusive max of 20 to future-proof against potential
  // increases in cycle count or shows-per-cycle without breaking histogram
  // compatibility. Current max is 9 (3 cycles * 3 shows).
  base::UmaHistogramExactLinear(
      "Windows.InstallerDownloader.TotalShowCount",
      local_state->GetInteger(prefs::kInstallerDownloaderTotalShowCount), 20);
}

bool InstallerDownloaderModelImpl::IsCurrentCycleFinished() const {
  const PrefService* local_state = g_browser_process->local_state();
  return local_state->GetBoolean(
             prefs::kInstallerDownloaderPreventFutureDisplay) ||
         local_state->GetInteger(prefs::kInstallerDownloaderInfobarShowCount) >=
             kMaxShowCount;
}

int InstallerDownloaderModelImpl::GetCurrentCycle() const {
  return std::max(1, g_browser_process->local_state()->GetInteger(
                         prefs::kInstallerDownloaderCycleCount));
}

void InstallerDownloaderModelImpl::OnInstallerDownloadCreated(
    const base::FilePath& expected_path,
    CompletionCallback completion_callback,
    download::DownloadItem* item,
    download::DownloadInterruptReason reason) {
  CHECK(!installer_downloader_observer_);

  // If `reason` is anything other than NONE (or we never got a DownloadItem)
  // will be treated as a failure.
  if (reason != download::DOWNLOAD_INTERRUPT_REASON_NONE || !item) {
    RecordDownloadInterruptReason(reason);
    std::move(completion_callback).Run(/*succeeded=*/false);
    return;
  }

  // The InstallerDownloaderController that hold this model is a browser global
  // feature. Therefore, it is safe to use base::Unretained here.
  installer_downloader_observer_ =
      std::make_unique<InstallerDownloaderObserver>(
          item, expected_path,
          base::BindOnce(
              &InstallerDownloaderModelImpl::OnInstallerDownloadFinished,
              base::Unretained(this), std::move(completion_callback)));
}

void InstallerDownloaderModelImpl::OnInstallerDownloadFinished(
    CompletionCallback completion_callback,
    bool succeeded) {
  installer_downloader_observer_.reset();
  std::move(completion_callback).Run(succeeded);
}

std::optional<base::FilePath>
InstallerDownloaderModelImpl::GetInstallerDestination() const {
  // 1) If this machine already meets the Win‑11 hardware requirements, an
  //    in‑place upgrade is possible, so there is no need for a standalone
  //    installer.
  if (system_info_provider_->IsHardwareEligibleForWin11()) {
    return std::nullopt;
  }

  // 2) Check OneDrive sync status (root + Desktop).
  cloud_synced_folder_checker::CloudSyncStatus cloud_sync_status =
      system_info_provider_->EvaluateOneDriveSyncStatus();

  // 3) Prefer the syncing Desktop folder; otherwise fall back to OneDrive root.
  if (cloud_sync_status.desktop_path.has_value()) {
    return std::move(cloud_sync_status.desktop_path);
  }

  return std::move(cloud_sync_status.one_drive_path);
}

}  // namespace installer_downloader
