// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/sync_device_info/device_name_util.h"

#include <optional>
#include <string_view>

#include "base/check.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/to_vector.h"
#include "base/i18n/unicodestring.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/base/features.h"
#include "components/sync_device_info/device_info.h"
#include "third_party/abseil-cpp/absl/strings/ascii.h"
#include "third_party/icu/source/common/unicode/unistr.h"
#include "ui/base/l10n/l10n_util.h"

namespace syncer {

namespace {

constexpr char kWindowsDesktopPrefix[] = "DESKTOP-";
constexpr char kWindowsLaptopPrefix[] = "LAPTOP-";

// Returns the localized string resource ID of the device type based on its
// form factor, or Windows generic device type if applicable.
int GetDeviceNameFormatStringId(const DeviceInfo& device) {
  if (device.os_type() == DeviceInfo::OsType::kWindows &&
      base::FeatureList::IsEnabled(kSyncSimplifyDeviceNaming)) {
    if (base::StartsWith(device.client_name(), kWindowsDesktopPrefix,
                         base::CompareCase::SENSITIVE)) {
      return IDS_SYNC_DEVICE_NAME_DESKTOP_FORMAT;
    }
    if (base::StartsWith(device.client_name(), kWindowsLaptopPrefix,
                         base::CompareCase::SENSITIVE)) {
      return IDS_SYNC_DEVICE_NAME_LAPTOP_FORMAT;
    }
  }

  switch (device.form_factor()) {
    case DeviceInfo::FormFactor::kDesktop:
      return IDS_SYNC_DEVICE_NAME_COMPUTER_FORMAT;
    case DeviceInfo::FormFactor::kPhone:
      return IDS_SYNC_DEVICE_NAME_PHONE_FORMAT;
    case DeviceInfo::FormFactor::kTablet:
      return IDS_SYNC_DEVICE_NAME_TABLET_FORMAT;
    case DeviceInfo::FormFactor::kAutomotive:
    case DeviceInfo::FormFactor::kWearable:
    case DeviceInfo::FormFactor::kTv:
    case DeviceInfo::FormFactor::kUnknown:
      return IDS_SYNC_DEVICE_NAME_DEVICE_FORMAT;
  }
  NOTREACHED();
}

// Capitalizes the first letter of the string and any letter that immediately
// follows a non-alphabetic character, using ICU for locale-aware title casing.
std::string CapitalizeWords(const std::string& sentence) {
  std::u16string utf16_sentence = base::UTF8ToUTF16(sentence);
  icu::UnicodeString unicode_sentence(utf16_sentence.data(),
                                      utf16_sentence.length());
  // Pass nullptr to use the default titlecase break iterator, which identifies
  // word boundaries using standard Unicode rules.
  unicode_sentence.toTitle(/*titleIter=*/nullptr);
  return base::UTF16ToUTF8(
      base::i18n::UnicodeStringToString16(unicode_sentence));
}

// Returns true if the client name looks like a Windows auto-generated name.
// Windows auto-generated names are exactly 15 characters long, containing a
// prefix (up to 7 characters, derived from user/org name or
// "DESKTOP"/"LAPTOP"), a hyphen, and a random suffix. See:
// https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup-computername#values
bool IsWindowsNameLikelyAutogenerated(std::string_view name) {
  // Windows auto-generated names are exactly 15 characters long (NetBIOS
  // limit).
  if (name.length() != 15) {
    return false;
  }

  // There must be a hyphen, and the prefix before the hyphen must be between
  // 1 and 7 chars long.
  size_t hyphen_pos = name.find('-');
  if (hyphen_pos == std::string::npos || hyphen_pos < 1 || hyphen_pos > 7) {
    return false;
  }

  // Windows auto-generated names consist of uppercase alphanumeric characters
  // and a single hyphen.
  for (size_t i = 0; i < name.length(); ++i) {
    if (i == hyphen_pos) {
      continue;
    }
    char c = name[i];
    if (!absl::ascii_isupper(static_cast<unsigned char>(c)) &&
        !absl::ascii_isdigit(static_cast<unsigned char>(c))) {
      return false;
    }
  }

  return true;
}

bool IsClientNameHighQuality(const DeviceInfo* device) {
  const std::string model = device->model_name();
  const std::string client_name = device->client_name();

  if (client_name.empty() || client_name == model) {
    return false;
  }

  // On iOS 16+, the default client name is "iPhone" or "iPad". It is not a
  // high-quality name, so we shouldn't treat it as a custom name.
  // See
  // https://developer.apple.com/documentation/uikit/uidevice/name#Discussion
  if (device->os_type() == DeviceInfo::OsType::kIOS) {
    if (client_name == "iPhone" || client_name == "iPad") {
      return false;
    }
  }

  if (device->os_type() == DeviceInfo::OsType::kWindows &&
      base::FeatureList::IsEnabled(kSyncSimplifyDeviceNaming)) {
    if (IsWindowsNameLikelyAutogenerated(client_name)) {
      return false;
    }
  }

  return true;
}

// Returns the release channel label for non-stable devices, or nullopt if the
// device is on the Stable channel or has an unknown release channel.
std::optional<std::string> GetDisambiguationLabel(const DeviceInfo* device) {
  const std::string& user_agent = device->sync_user_agent();
  if (user_agent.ends_with("channel(canary)")) {
    return l10n_util::GetStringUTF8(IDS_SYNC_DEVICE_NAME_CANARY_CHANNEL);
  }
  if (user_agent.ends_with("channel(dev)")) {
    return l10n_util::GetStringUTF8(IDS_SYNC_DEVICE_NAME_DEV_CHANNEL);
  }
  if (user_agent.ends_with("channel(beta)")) {
    return l10n_util::GetStringUTF8(IDS_SYNC_DEVICE_NAME_BETA_CHANNEL);
  }
  if (user_agent.ends_with("-devel")) {
    return l10n_util::GetStringUTF8(IDS_SYNC_DEVICE_NAME_DEVELOPER_BUILD);
  }
  // Devices on the Stable channel or with unknown user agents return nullopt.
  return std::nullopt;
}

std::string FormatNameWithDisambiguation(std::string_view base_name,
                                         std::string_view label) {
  return l10n_util::GetStringFUTF8(
      IDS_SYNC_DEVICE_NAME_WITH_DISAMBIGUATION_FORMAT,
      base::UTF8ToUTF16(base_name), base::UTF8ToUTF16(label));
}

// Helper class for `GetDeviceDisplayNames()` that tracks display name
// frequencies across target devices and an active local device to determine
// whether release channel labels are required.
class DeviceNameDisambiguator {
 public:
  DeviceNameDisambiguator(const std::vector<const DeviceInfo*>& devices,
                          const DeviceInfo* local_device);

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

  ~DeviceNameDisambiguator() = default;

  std::string GetDisambiguatedDisplayName(const DeviceInfo* device) const;

 private:
  void AddDeviceToCounts(const DeviceInfo* device);

  base::flat_map<std::string, int> base_display_name_counts_;
};

void DeviceNameDisambiguator::AddDeviceToCounts(const DeviceInfo* device) {
  if (!device) {
    return;
  }
  ++base_display_name_counts_[GetDeviceDisplayName(device)];
}

DeviceNameDisambiguator::DeviceNameDisambiguator(
    const std::vector<const DeviceInfo*>& devices,
    const DeviceInfo* local_device) {
  AddDeviceToCounts(local_device);
  for (const DeviceInfo* device : devices) {
    AddDeviceToCounts(device);
  }
}

std::string DeviceNameDisambiguator::GetDisambiguatedDisplayName(
    const DeviceInfo* device) const {
  if (!device) {
    return std::string();
  }
  std::string base_display_name = GetDeviceDisplayName(device);
  auto base_it = base_display_name_counts_.find(base_display_name);
  if (base_it == base_display_name_counts_.end() || base_it->second <= 1) {
    // Base display name has no collisions across devices.
    return base_display_name;
  }
  // Format with release channel label only when necessary to disambiguate.
  std::optional<std::string> release_channel_label =
      GetDisambiguationLabel(device);
  if (release_channel_label.has_value()) {
    return FormatNameWithDisambiguation(base_display_name,
                                        *release_channel_label);
  }
  return base_display_name;
}

}  // namespace

DisplayNameCandidates GetDisplayNameCandidates(const DeviceInfo* device) {
  TRACE_EVENT0("sync", "syncer::GetDisplayNameCandidates");
  DCHECK(device);

  if (device->server_determined_model_name().has_value() &&
      !device->server_determined_model_name()->empty() &&
      base::FeatureList::IsEnabled(kSyncUseServerDeterminedDeviceName)) {
    std::string preferred_name = *device->server_determined_model_name();

    // Using the marketing name as the fallback as well, as naming collisions
    // are less likely with specific marketing names (e.g., "Galaxy S21" and
    // "Galaxy S17" instead of two "Samsung Phone"s).
    //
    // Additionally, appending the model name could result in redundant names
    // (e.g., "Pixel 9 Pixel 9") if the OEM has already populated the model
    // field with the marketing name.
    //
    // TODO(crbug.com/522788942): Remove this fallback construction once
    // kSyncUseServerDeterminedDeviceName and kSyncSimplifyDeviceNaming are
    // fully launched.
    return {.preferred_name_if_unique = preferred_name,
            .fallback_full_name = preferred_name};
  }

  const std::string model = device->model_name();
  const bool client_name_is_high_quality = IsClientNameHighQuality(device);

  // Skip renaming if client_name is high quality.
  if (client_name_is_high_quality) {
    return {.preferred_name_if_unique = device->client_name(),
            .fallback_full_name = device->client_name()};
  }

  std::string manufacturer = CapitalizeWords(device->manufacturer_name());

  // For chromeOS, return manufacturer + model.
  if (device->os_type() == DeviceInfo::OsType::kChromeOsAsh) {
    std::string name = base::StrCat({manufacturer, " ", model});
    return {.preferred_name_if_unique = name, .fallback_full_name = name};
  }

  // Internal names of Apple devices are formatted as MacbookPro2,3 or
  // iPhone2,1 or Ipad4,1.
  if (device->os_type() == DeviceInfo::OsType::kMac ||
      device->os_type() == DeviceInfo::OsType::kIOS) {
    std::string model_prefix =
        model.substr(0, model.find_first_of("0123456789,"));
    return {.preferred_name_if_unique = model_prefix,
            .fallback_full_name = model};
  }

  std::u16string preferred_name_if_unique = l10n_util::GetStringFUTF16(
      GetDeviceNameFormatStringId(*device), base::UTF8ToUTF16(manufacturer));
  std::u16string fallback_full_name = l10n_util::GetStringFUTF16(
      IDS_SYNC_DEVICE_NAME_WITH_MODEL_FORMAT, preferred_name_if_unique,
      base::UTF8ToUTF16(model));
  return {
      .preferred_name_if_unique = base::UTF16ToUTF8(preferred_name_if_unique),
      .fallback_full_name = base::UTF16ToUTF8(fallback_full_name)};
}

std::string GetDeviceDisplayName(const DeviceInfo* device) {
  CHECK(base::FeatureList::IsEnabled(kSyncSimplifyDeviceNaming));

  return GetDisplayNameCandidates(device).preferred_name_if_unique;
}

std::vector<std::string> GetDeviceDisplayNames(
    const std::vector<const DeviceInfo*>& devices,
    const DeviceInfo* local_device) {
  TRACE_EVENT0("sync", "syncer::GetDeviceDisplayNames");
  if (!base::FeatureList::IsEnabled(kSyncSimplifyDeviceNaming)) {
    std::optional<std::string> local_device_name =
        local_device
            ? std::make_optional(
                  GetDisplayNameCandidates(local_device).fallback_full_name)
            : std::nullopt;
    std::vector<DeviceInfoWithName> legacy_names =
        DetermineDisplayNamesAndDeduplicate(devices, local_device_name);
    return base::ToVector(legacy_names, [](const DeviceInfoWithName& info) {
      return info.display_name;
    });
  }

  if (base::FeatureList::IsEnabled(kSyncDisambiguateDeviceNamesWithChannel)) {
    DeviceNameDisambiguator disambiguator(devices, local_device);
    return base::ToVector(devices, [&](const DeviceInfo* device) {
      return disambiguator.GetDisambiguatedDisplayName(device);
    });
  }

  return base::ToVector(devices, [](const DeviceInfo* device) {
    return GetDeviceDisplayName(device);
  });
}

// `devices` should be sorted by recency (most recent first) to ensure that
// de-duplication keeps the most relevant device.
std::vector<DeviceInfoWithName> DetermineDisplayNamesAndDeduplicate(
    const std::vector<const DeviceInfo*>& devices,
    const std::optional<std::string>& local_device_name) {
  TRACE_EVENT0("sync", "syncer::DetermineDisplayNamesAndDeduplicate");
  struct DeviceEntry {
    raw_ptr<const DeviceInfo> device;
    DisplayNameCandidates candidates;
  };
  std::vector<DeviceEntry> filtered_devices;
  base::flat_set<std::string> seen_fallback_full_names;
  base::flat_map<std::string, int> preferred_names_counter;

  // 1. Initialize `seen_fallback_full_names` with local device's fallback full
  // name to prevent adding candidates with the same name.
  if (local_device_name) {
    seen_fallback_full_names.insert(*local_device_name);
  }

  // 2. Iterate through devices (expected to be sorted by recency) to:
  //    - De-duplicate by fallback full name.
  //    - Filter out devices with the same fallback full name as the local
  //      device.
  //    - Count preferred name if unique occurrences.
  for (const DeviceInfo* device : devices) {
    DisplayNameCandidates candidates = GetDisplayNameCandidates(device);

    // Filter out duplicates and local device.
    if (!seen_fallback_full_names.insert(candidates.fallback_full_name)
             .second) {
      continue;
    }

    ++preferred_names_counter[candidates.preferred_name_if_unique];
    filtered_devices.emplace_back(device, std::move(candidates));
  }

  // 3. Construct the final list.
  return base::ToVector(filtered_devices, [&](const DeviceEntry& entry) {
    return DeviceInfoWithName{
        .device = entry.device,
        .display_name =
            preferred_names_counter[entry.candidates
                                        .preferred_name_if_unique] == 1
                ? entry.candidates.preferred_name_if_unique
                : entry.candidates.fallback_full_name};
  });
}

}  // namespace syncer
