// Copyright 2021 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/ash/system_logs/reven_log_source.h"

#include <string_view>

#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/thread_pool.h"
#include "chromeos/ash/services/cros_healthd/public/cpp/service_connection.h"
#include "chromeos/ash/services/cros_healthd/public/mojom/cros_healthd_probe.mojom.h"

namespace system_logs {

namespace {

namespace healthd = ::ash::cros_healthd::mojom;
using healthd::TelemetryInfo;
using healthd::TelemetryInfoPtr;
using ProbeCategories = healthd::ProbeCategoryEnum;

constexpr char kNotAvailable[] = "<not available>";

constexpr char kRevenAvailableMemoryKey[] = "chromeosflex_available_memory";
constexpr char kRevenBiosVersionKey[] = "chromeosflex_bios_version";
constexpr char kRevenBluetoothDriverKey[] = "chromeosflex_bluetooth_driver";
constexpr char kRevenBluetoothIdKey[] = "chromeosflex_bluetooth_id";
constexpr char kRevenBluetoothNameKey[] = "chromeosflex_bluetooth_name";
constexpr char kRevenCpuNameKey[] = "chromeosflex_cpu_name";
constexpr char kRevenEthernetDriverKey[] = "chromeosflex_ethernet_driver";
constexpr char kRevenEthernetIdKey[] = "chromeosflex_ethernet_id";
constexpr char kRevenEthernetNameKey[] = "chromeosflex_ethernet_name";
constexpr char kRevenFreeMemoryKey[] = "chromeosflex_free_memory";
constexpr char kRevenGlExtensionsKey[] = "chromeosflex_gl_extensions";
constexpr char kRevenGlRendererKey[] = "chromeosflex_gl_renderer";
constexpr char kRevenGlShadingVersionKey[] = "chromeosflex_gl_shading_version";
constexpr char kRevenGlVendorKey[] = "chromeosflex_gl_vendor";
constexpr char kRevenGlVersionKey[] = "chromeosflex_gl_version";
constexpr char kRevenGpuDriverKey[] = "chromeosflex_gpu_driver";
constexpr char kRevenGpuIdKey[] = "chromeosflex_gpu_id";
constexpr char kRevenGpuNameKey[] = "chromeosflex_gpu_name";
constexpr char kRevenProductNameKey[] = "chromeosflex_product_name";
constexpr char kRevenProductVendorKey[] = "chromeosflex_product_vendor";
constexpr char kRevenProductVersionKey[] = "chromeosflex_product_version";
constexpr char kRevenSecurebootKey[] = "chromeosflex_secureboot";
constexpr char kRevenTotalMemoryKey[] = "chromeosflex_total_memory";
constexpr char kRevenTouchpadStack[] = "chromeosflex_touchpad";
constexpr char kRevenTpmAllowListedKey[] = "chromeosflex_tpm_allow_listed";
constexpr char kRevenTpmDidVidKey[] = "chromeosflex_tpm_did_vid";
constexpr char kRevenTpmManufacturerKey[] = "chromeosflex_tpm_manufacturer";
constexpr char kRevenTpmOwnedKey[] = "chromeosflex_tpm_owned";
constexpr char kRevenTpmSpecLevelKey[] = "chromeosflex_tpm_spec_level";
constexpr char kRevenTpmVersionKey[] = "chromeosflex_tpm_version";
constexpr char kRevenUefiKey[] = "chromeosflex_uefi";
constexpr char kRevenWirelessDriverKey[] = "chromeosflex_wireless_driver";
constexpr char kRevenWirelessIdKey[] = "chromeosflex_wireless_id";
constexpr char kRevenWirelessNameKey[] = "chromeosflex_wireless_name";

// Format a gradually-accumulated, comma-separated list.
void StrListAppend(std::string* list, std::string_view value) {
  if (!list->empty()) {
    base::StrAppend(list, {", "});
  }
  base::StrAppend(list, {value});
}

// Format the combination of bus, vendor_id, and product_id or device_id.
std::string FormatDeviceIDs(const std::string& bus,
                            uint16_t vendor_id,
                            uint16_t product_id) {
  return base::StringPrintf("%s:%04x:%04x", bus.c_str(), vendor_id, product_id);
}

// TPM family. We use the TPM 2.0 style encoding, e.g.:
//  * TPM 1.2: "1.2" -> 0x312e3200
//  * TPM 2.0: "2.0" -> 0x322e3000
std::string ToTpmVersionStr(uint32_t tpm_family) {
  if (tpm_family == (uint32_t)0x312e3200) {
    return "1.2";
  } else if (tpm_family == (uint32_t)0x322e3000) {
    return "2.0";
  } else {
    return "unknown";
  }
}

void PopulateCpuInfo(SystemLogsResponse& psd, const TelemetryInfoPtr& info) {
  if (info->cpu_result.is_null() || info->cpu_result->is_error()) {
    DVLOG(1) << "CpuResult not found in croshealthd response";
    return;
  }
  std::vector<healthd::PhysicalCpuInfoPtr>& physical_cpus =
      info->cpu_result->get_cpu_info()->physical_cpus;
  DCHECK_GE(physical_cpus.size(), 1u);

  std::string cpu_names;
  for (const auto& cpu : physical_cpus) {
    // Missing/empty values will be distinguishable in the final list.
    StrListAppend(&cpu_names, cpu->model_name.value_or(kNotAvailable));
  }
  psd.emplace(kRevenCpuNameKey, cpu_names);
}

void PopulateMemoryInfo(SystemLogsResponse& psd, const TelemetryInfoPtr& info) {
  if (info->memory_result.is_null() || info->memory_result->is_error()) {
    DVLOG(1) << "MemoryResult not found in croshealthd response";
    return;
  }

  healthd::MemoryInfoPtr& memory_info = info->memory_result->get_memory_info();

  psd.emplace(kRevenTotalMemoryKey,
              base::NumberToString(memory_info->total_memory_kib));
  psd.emplace(kRevenFreeMemoryKey,
              base::NumberToString(memory_info->free_memory_kib));
  psd.emplace(kRevenAvailableMemoryKey,
              base::NumberToString(memory_info->available_memory_kib));
}

void PopulateSystemInfo(SystemLogsResponse& psd, const TelemetryInfoPtr& info) {
  if (info->system_result.is_null() || info->system_result->is_error()) {
    DVLOG(1) << "SystemResult not found in croshealthd response";
    return;
  }

  healthd::DmiInfoPtr& dmi_info =
      info->system_result->get_system_info()->dmi_info;
  if (!dmi_info.is_null()) {
    psd.emplace(kRevenProductVendorKey,
                dmi_info->sys_vendor.value_or(kNotAvailable));
    psd.emplace(kRevenProductNameKey,
                dmi_info->product_name.value_or(kNotAvailable));
    psd.emplace(kRevenProductVersionKey,
                dmi_info->product_version.value_or(kNotAvailable));
    psd.emplace(kRevenBiosVersionKey,
                dmi_info->bios_version.value_or(kNotAvailable));
  }

  healthd::OsInfoPtr& os_info = info->system_result->get_system_info()->os_info;
  if (!os_info.is_null()) {
    psd.emplace(kRevenSecurebootKey,
                base::ToString(os_info->boot_mode ==
                               healthd::BootMode::kCrosEfiSecure));
    psd.emplace(kRevenUefiKey,
                base::ToString(
                    os_info->boot_mode == healthd::BootMode::kCrosEfi ||
                    os_info->boot_mode == healthd::BootMode::kCrosEfiSecure));
  }
}

// Constructs key names based on the passed label. Collects data from all passed
// devices into each value.
void PopulateBusDevicesInfo(SystemLogsResponse& psd,
                            std::string_view label,
                            const std::vector<healthd::BusDevicePtr>& devices) {
  if (devices.empty()) {
    return;
  }

  const std::string name_key = base::StrCat({"chromeosflex_", label, "_name"});
  std::string names;
  const std::string id_key = base::StrCat({"chromeosflex_", label, "_id"});
  std::string ids;
  const std::string driver_key =
      base::StrCat({"chromeosflex_", label, "_driver"});
  std::string drivers;

  for (const auto& device : devices) {
    StrListAppend(
        &names, base::StrCat({device->vendor_name, " ", device->product_name}));

    if (device->bus_info->is_pci_bus_info()) {
      const healthd::PciBusInfoPtr& pci_info =
          device->bus_info->get_pci_bus_info();

      StrListAppend(&ids, FormatDeviceIDs("pci", pci_info->vendor_id,
                                          pci_info->device_id));

      // Missing/empty values will be distinguishable in the final list.
      StrListAppend(&drivers, pci_info->driver.value_or(kNotAvailable));
    }

    if (device->bus_info->is_usb_bus_info()) {
      const healthd::UsbBusInfoPtr& usb_info =
          device->bus_info->get_usb_bus_info();

      StrListAppend(&ids, FormatDeviceIDs("usb", usb_info->vendor_id,
                                          usb_info->product_id));

      const std::vector<healthd::UsbBusInterfaceInfoPtr>& usb_interfaces =
          usb_info->interfaces;
      for (const auto& interface : usb_interfaces) {
        // Missing/empty values will be distinguishable in the final list.
        StrListAppend(&drivers, interface->driver.value_or(kNotAvailable));
      }
    }
  }

  psd.emplace(name_key, names);
  psd.emplace(id_key, ids);
  psd.emplace(driver_key, drivers);
}

void PopulateBusDevicesInfo(SystemLogsResponse& psd,
                            const TelemetryInfoPtr& info) {
  if (info->bus_result.is_null() || info->bus_result->is_error()) {
    DVLOG(1) << "BusResult not found in croshealthd response";
    return;
  }
  std::vector<healthd::BusDevicePtr>& bus_devices =
      info->bus_result->get_bus_devices();

  // When looking at bus devices there can be multiple of any given class
  // (though in practice we mostly see this with gpu). We need to accumulate
  // these values on a per-class basis, so: sort by class we care about, then
  // process in a group.
  std::vector<healthd::BusDevicePtr> ethernet_devices;
  std::vector<healthd::BusDevicePtr> wireless_devices;
  std::vector<healthd::BusDevicePtr> bluetooth_devices;
  std::vector<healthd::BusDevicePtr> gpu_devices;
  for (auto& device : bus_devices) {
    switch (device->device_class) {
      case healthd::BusDeviceClass::kEthernetController:
        ethernet_devices.push_back(std::move(device));
        break;
      case healthd::BusDeviceClass::kWirelessController:
        wireless_devices.push_back(std::move(device));
        break;
      case healthd::BusDeviceClass::kBluetoothAdapter:
        bluetooth_devices.push_back(std::move(device));
        break;
      case healthd::BusDeviceClass::kDisplayController:
        gpu_devices.push_back(std::move(device));
        break;
      default:
        break;
    }
  }

  // See kRevenEthernet*Key, kRevenWireless*Key,
  // kRevenBluetooth*Key, and kRevenGpu*Key.
  PopulateBusDevicesInfo(psd, "ethernet", ethernet_devices);
  PopulateBusDevicesInfo(psd, "wireless", wireless_devices);
  PopulateBusDevicesInfo(psd, "bluetooth", bluetooth_devices);
  PopulateBusDevicesInfo(psd, "gpu", gpu_devices);
}

void PopulateTpmInfo(SystemLogsResponse& psd, const TelemetryInfoPtr& info) {
  if (info->tpm_result.is_null() || info->tpm_result->is_error()) {
    DVLOG(1) << "TpmResult not found in croshealthd response";
    return;
  }
  const healthd::TpmInfoPtr& tpm_info = info->tpm_result->get_tpm_info();
  if (tpm_info.is_null()) {
    return;
  }

  psd.emplace(kRevenTpmDidVidKey, tpm_info->did_vid.value_or(kNotAvailable));
  psd.emplace(kRevenTpmAllowListedKey,
              base::ToString(tpm_info->supported_features->is_allowed));
  psd.emplace(kRevenTpmOwnedKey, base::ToString(tpm_info->status->owned));

  const healthd::TpmVersionPtr& version = tpm_info->version;
  if (version.is_null()) {
    return;
  }

  psd.emplace(kRevenTpmVersionKey, ToTpmVersionStr(version->family));
  psd.emplace(kRevenTpmSpecLevelKey, base::NumberToString(version->spec_level));
  psd.emplace(kRevenTpmManufacturerKey,
              base::NumberToString(version->manufacturer));
}

void PopulateGraphicsInfo(SystemLogsResponse& psd,
                          const TelemetryInfoPtr& info) {
  if (info->graphics_result.is_null() || info->graphics_result->is_error()) {
    DVLOG(1) << "GraphicsResult not found in croshealthd response";
    return;
  }
  const healthd::GraphicsInfoPtr& graphics_info =
      info->graphics_result->get_graphics_info();

  const healthd::GLESInfoPtr& gles_info = graphics_info->gles_info;
  psd.emplace(kRevenGlVersionKey, gles_info->version);
  psd.emplace(kRevenGlShadingVersionKey, gles_info->shading_version);
  psd.emplace(kRevenGlVendorKey, gles_info->vendor);
  psd.emplace(kRevenGlRendererKey, gles_info->renderer);
  psd.emplace(kRevenGlExtensionsKey,
              base::JoinString(gles_info->extensions, ", "));
}

std::string FetchTouchpadLibraryName() {
  return ash::cros_healthd::ServiceConnection::GetInstance()
      ->FetchTouchpadLibraryName();
}

// For any key not set, set it to "<not available>".
// We use the sentinel value to distinguish between "empty" data and data that
// couldn't be retrieved. We set all values to help keep reporting consistent.
void SetNotAvailablePsd(SystemLogsResponse& psd) {
  psd.try_emplace(kRevenAvailableMemoryKey, kNotAvailable);
  psd.try_emplace(kRevenBiosVersionKey, kNotAvailable);
  psd.try_emplace(kRevenBluetoothDriverKey, kNotAvailable);
  psd.try_emplace(kRevenBluetoothIdKey, kNotAvailable);
  psd.try_emplace(kRevenBluetoothNameKey, kNotAvailable);
  psd.try_emplace(kRevenCpuNameKey, kNotAvailable);
  psd.try_emplace(kRevenEthernetDriverKey, kNotAvailable);
  psd.try_emplace(kRevenEthernetIdKey, kNotAvailable);
  psd.try_emplace(kRevenEthernetNameKey, kNotAvailable);
  psd.try_emplace(kRevenFreeMemoryKey, kNotAvailable);
  psd.try_emplace(kRevenGlExtensionsKey, kNotAvailable);
  psd.try_emplace(kRevenGlRendererKey, kNotAvailable);
  psd.try_emplace(kRevenGlShadingVersionKey, kNotAvailable);
  psd.try_emplace(kRevenGlVendorKey, kNotAvailable);
  psd.try_emplace(kRevenGlVersionKey, kNotAvailable);
  psd.try_emplace(kRevenGpuDriverKey, kNotAvailable);
  psd.try_emplace(kRevenGpuIdKey, kNotAvailable);
  psd.try_emplace(kRevenGpuNameKey, kNotAvailable);
  psd.try_emplace(kRevenProductNameKey, kNotAvailable);
  psd.try_emplace(kRevenProductVendorKey, kNotAvailable);
  psd.try_emplace(kRevenProductVersionKey, kNotAvailable);
  psd.try_emplace(kRevenSecurebootKey, kNotAvailable);
  psd.try_emplace(kRevenTotalMemoryKey, kNotAvailable);
  psd.try_emplace(kRevenTpmAllowListedKey, kNotAvailable);
  psd.try_emplace(kRevenTpmDidVidKey, kNotAvailable);
  psd.try_emplace(kRevenTpmManufacturerKey, kNotAvailable);
  psd.try_emplace(kRevenTpmOwnedKey, kNotAvailable);
  psd.try_emplace(kRevenTpmSpecLevelKey, kNotAvailable);
  psd.try_emplace(kRevenTpmVersionKey, kNotAvailable);
  psd.try_emplace(kRevenUefiKey, kNotAvailable);
  psd.try_emplace(kRevenWirelessDriverKey, kNotAvailable);
  psd.try_emplace(kRevenWirelessIdKey, kNotAvailable);
  psd.try_emplace(kRevenWirelessNameKey, kNotAvailable);
}

}  // namespace

RevenLogSource::RevenLogSource() : SystemLogsSource("Reven") {
  ash::cros_healthd::ServiceConnection::GetInstance()->BindProbeService(
      probe_service_.BindNewPipeAndPassReceiver());
}

RevenLogSource::~RevenLogSource() = default;

void RevenLogSource::Fetch(SysLogsSourceCallback callback) {
  probe_service_->ProbeTelemetryInfo(
      {ProbeCategories::kBluetooth, ProbeCategories::kBus,
       ProbeCategories::kCpu, ProbeCategories::kGraphics,
       ProbeCategories::kMemory, ProbeCategories::kSystem,
       ProbeCategories::kTpm},
      base::BindOnce(&RevenLogSource::OnTelemetryInfoProbeResponse,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void RevenLogSource::OnTelemetryInfoProbeResponse(
    SysLogsSourceCallback callback,
    TelemetryInfoPtr info_ptr) {
  SystemLogsResponse product_specific_data;

  if (info_ptr.is_null()) {
    DVLOG(1) << "Null response from croshealthd::ProbeTelemetryInfo.";
  }

  PopulateSystemInfo(product_specific_data, info_ptr);
  PopulateCpuInfo(product_specific_data, info_ptr);
  PopulateMemoryInfo(product_specific_data, info_ptr);
  PopulateBusDevicesInfo(product_specific_data, info_ptr);
  PopulateTpmInfo(product_specific_data, info_ptr);
  PopulateGraphicsInfo(product_specific_data, info_ptr);

  // Make sure all keys are present.
  SetNotAvailablePsd(product_specific_data);

  base::OnceCallback<void(const std::string&)> reply_cb = base::BindOnce(
      [](SysLogsSourceCallback callback, const SystemLogsResponse& psd,
         const std::string& touchpad_lib_name) {
        auto response = std::make_unique<SystemLogsResponse>(psd);
        // Make sure to overwrite the "not available".
        response->emplace(kRevenTouchpadStack, touchpad_lib_name);
        std::move(callback).Run(std::move(response));
      },
      std::move(callback), product_specific_data);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&FetchTouchpadLibraryName), std::move(reply_cb));
}

}  // namespace system_logs
