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

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

#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/strings/escape.h"
#include "base/strings/stringprintf.h"
#include "base/version_info/version_info.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/common/channel_info.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"

#if BUILDFLAG(IS_CHROMEOS) && BUILDFLAG(GOOGLE_CHROME_BRANDING)
#include "base/system/sys_info.h"
#include "chromeos/ash/components/channel/channel_info.h"
#endif  // BUILDFLAG(IS_CHROMEOS) && BUILDFLAG(GOOGLE_CHROME_BRANDING)

namespace {

// Returns the name of the current channel, as ingested by the VersionHistory
// API.
std::string GetChannelString() {
  std::string channel =
      chrome::GetChannelName(chrome::WithExtendedStable(true));
  if (channel == "unknown") {
    return "stable";
  }
  if (!channel.empty()) {
    return channel;
  }
#if BUILDFLAG(IS_CHROMEOS) && BUILDFLAG(GOOGLE_CHROME_BRANDING)
  // "" could mean Stable, LTC, or LTS. Find out which.
  std::string ash_channel_name;
  if (base::SysInfo::GetLsbReleaseValue(ash::kChromeOSReleaseTrack,
                                        &ash_channel_name)) {
    if (ash_channel_name == ash::kReleaseChannelLtc) {
      return "ltc";
    }
    if (ash_channel_name == ash::kReleaseChannelLts) {
      return "lts";
    }
  }
#endif  // BUILDFLAG(IS_CHROMEOS) && BUILDFLAG(GOOGLE_CHROME_BRANDING)
  return "stable";
}

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("version_history", R"(
        semantics {
          sender: "VersionHistory Client"
          description:
            "Queries the VersionHistory API to know how old the "
            "currently-running version of Chrome is. If it's older than a "
            "threshold configured by the admin, Chrome forces a relaunch (or "
            "ChromeOS forces a restart) to apply the update."
          trigger:
            "When Chrome detects that an update is available, if the "
            "RelaunchFastIfOutdated policy is set."
          data: "The version number of the currently-running Chrome/ChromeOS, "
            "the update channel name, and an identifier of the platform."
          destination: GOOGLE_OWNED_SERVICE
          user_data {
            type: NONE
          }
          internal {
             contacts {
               email: "cec-growth-eng@google.com"
             }
          }
          last_reviewed: "2025-03-06"
        }
        policy {
          cookies_allowed: NO
          setting: "This feature cannot be disabled by settings."
          chrome_policy {
            RelaunchFastIfOutdated {
              RelaunchFastIfOutdated: 0
            }
          }
        })");

// Sends a GET to `url`, and calls `callback` with the response.
void FetchUrl(GURL url,
              base::OnceCallback<void(std::unique_ptr<network::SimpleURLLoader>,
                                      std::optional<std::string>)> callback) {
  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
      g_browser_process->shared_url_loader_factory();

  auto request = std::make_unique<network::ResourceRequest>();
  request->url = std::move(url);
  request->load_flags = net::LOAD_DISABLE_CACHE | net::LOAD_BYPASS_CACHE;
  request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  request->priority = net::IDLE;

  std::unique_ptr<network::SimpleURLLoader> url_loader =
      network::SimpleURLLoader::Create(std::move(request), kTrafficAnnotation);
  url_loader->SetRetryOptions(
      /*max_retries=*/3,
      network::SimpleURLLoader::RetryMode::RETRY_ON_NETWORK_CHANGE);

  network::SimpleURLLoader* url_loader_raw = url_loader.get();
  // Move `url_loader` to the callback so the request can finish.
  url_loader_raw->DownloadToString(
      url_loader_factory.get(),
      base::BindOnce(std::move(callback), std::move(url_loader)),
      /*max_body_size=*/1024 * 1024);
}

// Parses and extract the most recent `serving.endTime` from `raw_data` JSON.
std::optional<base::Time> OnVersionReleasesFetched(
    std::unique_ptr<network::SimpleURLLoader> url_loader,
    std::optional<std::string> raw_data) {
  if (!raw_data) {
    return std::nullopt;
  }

  std::optional<base::DictValue> json = base::JSONReader::ReadDict(
      *raw_data, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!json) {
    return std::nullopt;
  }

  const base::ListValue* releases = json->FindList("releases");
  if (!releases || releases->empty()) {
    return std::nullopt;
  }

  // The first element is always the most recent, due to the "order_by" query
  // parameter.
  const base::Value& most_recent_release = releases->front();
  if (!most_recent_release.is_dict()) {
    return std::nullopt;
  }

  const std::string* end_time =
      most_recent_release.GetDict().FindStringByDottedPath("serving.endTime");
  if (!end_time) {
    // Builds with no end-time are still serving, so they're not outdated.
    return std::nullopt;
  }

  base::Time time;
  if (!base::Time::FromUTCString(end_time->c_str(), &time)) {
    return std::nullopt;
  }

  return time;
}

}  // namespace

// Returns the URL to get version info of `version` on the current platform
// and channel. The response contains the single release with the most recent
// `serving.endTime`.
GURL GetVersionReleasesUrl(base::Version version) {
// CURRENT_PLATFORM is the platform name, as ingested by the VersionHistory API.
// Use #define instead of a constant, so it concatenates at compile-time.
#if BUILDFLAG(IS_WIN)

#if defined(ARCH_CPU_ARM64)
#define CURRENT_PLATFORM "win_arm64"
#elif defined(ARCH_CPU_X86_64)
#define CURRENT_PLATFORM "win64"
#else
#define CURRENT_PLATFORM "win"
#endif

#elif BUILDFLAG(IS_LINUX)

#define CURRENT_PLATFORM "linux"

#elif BUILDFLAG(IS_MAC)

#if defined(ARCH_CPU_ARM64)
#define CURRENT_PLATFORM "mac_arm64"
#else
#define CURRENT_PLATFORM "mac"
#endif

#elif BUILDFLAG(IS_CHROMEOS)

#define CURRENT_PLATFORM "chromeos"

#else

#error Unsupported platform

#endif  // BUILDFLAG(IS_WIN)

  return GURL(base::StringPrintf(
      "https://versionhistory.googleapis.com/v1/chrome/"
      "platforms/" CURRENT_PLATFORM
      "/channels/%s/versions/%s/releases/?order_by=endtime%%20desc&page_size=1",
      GetChannelString(), version.GetString()));
#undef CURRENT_PLATFORM
}

void GetLastServedDate(base::Version version, LastServedDateCallback callback) {
  FetchUrl(GetVersionReleasesUrl(std::move(version)),
           base::BindOnce(&OnVersionReleasesFetched).Then(std::move(callback)));
}
