// Copyright 2012 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/debug_daemon_log_source.h"

#include <stddef.h>

#include <algorithm>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/os_feedback/chrome_os_feedback_delegate.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/common/chrome_switches.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "components/feedback/feedback_util.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_thread.h"

namespace system_logs {

namespace {

constexpr char kEmpty[] = "<empty>";
constexpr char kNotAvailable[] = "<not available>";
constexpr char kRoutesKeyName[] = "routes";
constexpr char kRoutesv6KeyName[] = "routes6";
constexpr char kLogTruncated[] = "<earlier logs truncated>\n";

// List of user log files that Chrome reads directly as these logs are generated
// by Chrome itself.
constexpr struct UserLogs {
  // A string key used as a title for this log in feedback reports.
  const char* log_key;

  // The log file's path relative to the user's profile directory.
  const char* log_file_relative_path;

  // If true, |log_file_relative_path| is a pattern for which files to match.
  // This works like shell globbing. If there are multiple matches files, it
  // chooses the newest file, with the latest modified time.
  bool pattern = false;
} kUserLogs[] = {
    {"chrome_user_log", "log/chrome"},
    {"chrome_user_log.PREVIOUS", "log/chrome_????????-??????", true},
    {"libassistant_user_log", "google-assistant-library/log/libassistant.log"},
    {"login-times", "login-times"},
    {"logout-times", "logout-times"},
};

// List of debugd entries to exclude from the results.
constexpr std::array<const char*, 2> kExcludeList = {
    // Shill device and service properties are retrieved by ShillLogSource.
    "network-devices",
    "network-services",
};

// Buffer size for user logs in bytes. Given that maximum feedback report size
// is ~7M and that majority of log files are under 1M, we set a per-file limit
// of 1MiB.
const int64_t kMaxLogSize = 1024 * 1024;

std::vector<debugd::FeedbackLogType> GetLogTypesForUser(
    const user_manager::User* user) {
  // The default list of log types that we request from debugd.
  std::vector<debugd::FeedbackLogType> included_log_types = {
      debugd::FeedbackLogType::ARC_BUG_REPORT,
      debugd::FeedbackLogType::CONNECTIVITY_REPORT,
      debugd::FeedbackLogType::VERBOSE_COMMAND_LOGS,
      debugd::FeedbackLogType::COMMAND_LOGS,
      debugd::FeedbackLogType::FEEDBACK_LOGS,
      debugd::FeedbackLogType::BLUETOOTH_BQR,
      debugd::FeedbackLogType::LSB_RELEASE_INFO,
      debugd::FeedbackLogType::PERF_DATA,
      debugd::FeedbackLogType::OS_RELEASE_INFO,
      debugd::FeedbackLogType::VAR_LOG_FILES};
  if (user && ash::ChromeOsFeedbackDelegate::IsWifiDebugLogsAllowed(
                  user->GetProfilePrefs())) {
    // Include WIFI_FIRMWARE_DUMPS since it is allowed for the user.
    included_log_types.push_back(debugd::FeedbackLogType::WIFI_FIRMWARE_DUMPS);
  }

  return included_log_types;
}

}  // namespace

std::string ReadUserLogFile(const base::FilePath& log_file_path) {
  std::optional<std::string> maybe_value =
      feedback_util::ReadEndOfFile(log_file_path, kMaxLogSize);

  if (maybe_value.has_value() && maybe_value.value().size() == kMaxLogSize) {
    maybe_value.value().replace(0, strlen(kLogTruncated), kLogTruncated);

    LOG(WARNING) << "Large log file was likely truncated: " << log_file_path;
  }
  return (maybe_value.has_value() && !maybe_value.value().empty())
             ? maybe_value.value()
             : std::string(kNotAvailable);
}

std::string ReadUserLogFilePattern(
    const base::FilePath& log_file_path_pattern) {
  base::FilePath log_file_dir = log_file_path_pattern.DirName();
  base::FileEnumerator file_enumerator(
      log_file_dir, /*recursive=*/false, base::FileEnumerator::FILES,
      log_file_path_pattern.BaseName().value());

  base::Time newest_file_mtime;
  base::FilePath newest_file_path;
  for (base::FilePath path = file_enumerator.Next(); !path.empty();
       path = file_enumerator.Next()) {
    const base::FileEnumerator::FileInfo info = file_enumerator.GetInfo();
    if (newest_file_mtime.is_null() ||
        info.GetLastModifiedTime() >= newest_file_mtime) {
      newest_file_mtime = info.GetLastModifiedTime();
      newest_file_path = path;
    }
  }

  return newest_file_mtime.is_null() ? std::string(kNotAvailable)
                                     : ReadUserLogFile(newest_file_path);
}

// Reads the contents of the user log files listed in |kUserLogs| and adds them
// to the |response| parameter.
void ReadUserLogFiles(const std::vector<base::FilePath>& profile_dirs,
                      SystemLogsResponse* response) {
  for (size_t i = 0; i < profile_dirs.size(); ++i) {
    std::string profile_prefix = "Profile[" + base::NumberToString(i) + "] ";
    const base::FilePath profile_dir = profile_dirs[i];
    for (const auto& log : kUserLogs) {
      const base::FilePath log_file_path_or_pattern =
          profile_dir.AppendASCII(log.log_file_relative_path);
      const std::string content =
          log.pattern ? ReadUserLogFilePattern(log_file_path_or_pattern)
                      : ReadUserLogFile(log_file_path_or_pattern);
      response->emplace(profile_prefix + log.log_key, content);
    }
  }
}

DebugDaemonLogSource::DebugDaemonLogSource(bool scrub)
    : SystemLogsSource("DebugDemon"),
      response_(new SystemLogsResponse()),
      num_pending_requests_(0),
      scrub_(scrub) {}

DebugDaemonLogSource::~DebugDaemonLogSource() = default;

void DebugDaemonLogSource::Fetch(SysLogsSourceCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!callback.is_null());
  DCHECK(callback_.is_null());

  callback_ = std::move(callback);
  ash::DebugDaemonClient* client = ash::DebugDaemonClient::Get();

  client->GetRoutes(true,   // Numeric
                    false,  // No IPv6
                    false,  // All tables option disabled
                    base::BindOnce(&DebugDaemonLogSource::OnGetRoutes,
                                   weak_ptr_factory_.GetWeakPtr(), false));
  ++num_pending_requests_;
  client->GetRoutes(true,   // Numeric
                    true,   // with IPv6
                    false,  // All tables option disabled
                    base::BindOnce(&DebugDaemonLogSource::OnGetRoutes,
                                   weak_ptr_factory_.GetWeakPtr(), true));
  ++num_pending_requests_;

  const auto start_time = base::TimeTicks::Now();
  if (scrub_) {
    const user_manager::User* user =
        user_manager::UserManager::Get()->GetActiveUser();
    const auto account_identifier =
        cryptohome::CreateAccountIdentifierFromAccountId(
            user ? user->GetAccountId() : EmptyAccountId());

    client->GetFeedbackLogs(
        account_identifier, GetLogTypesForUser(user),
        base::BindOnce(&DebugDaemonLogSource::OnGetLogs,
                       weak_ptr_factory_.GetWeakPtr(), start_time));

  } else {
    client->GetAllLogs(base::BindOnce(&DebugDaemonLogSource::OnGetLogs,
                                      weak_ptr_factory_.GetWeakPtr(),
                                      start_time));
  }
  ++num_pending_requests_;
}

void DebugDaemonLogSource::OnGetRoutes(
    bool is_ipv6,
    std::optional<std::vector<std::string>> routes) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  std::string key = is_ipv6 ? kRoutesv6KeyName : kRoutesKeyName;
  (*response_)[key] = routes.has_value()
                          ? base::JoinString(routes.value(), "\n")
                          : kNotAvailable;
  RequestCompleted();
}

void DebugDaemonLogSource::OnGetOneLog(std::string key,
                                       std::optional<std::string> status) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  (*response_)[std::move(key)] = std::move(status).value_or(kNotAvailable);
  RequestCompleted();
}

void DebugDaemonLogSource::OnGetLogs(const base::TimeTicks get_start_time,
                                     bool succeeded,
                                     const KeyValueMap& logs) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // We are interested in the performance of gathering the logs for feedback
  // reports only where the logs will always be scrubbed. GetBigFeedbackLogs is
  // the dbus method used.
  if (scrub_) {
    base::UmaHistogramBoolean("Feedback.ChromeOSApp.GetBigFeedbackLogs.Success",
                              succeeded);
    base::UmaHistogramMediumTimes(
        "Feedback.ChromeOSApp.Duration.GetBigFeedbackLogs",
        base::TimeTicks::Now() - get_start_time);
  }
  int empty_log_count = 0;
  int not_available_log_count = 0;
  int other_log_count = 0;

  // We ignore 'succeeded' for this callback - we want to display as much of the
  // debug info as we can even if we failed partway through parsing, and if we
  // couldn't fetch any of it, none of the fields will even appear.
  for (const auto& log : logs) {
    if (std::ranges::contains(kExcludeList, log.first)) {
      continue;
    }
    response_->insert(log);
    if (log.second == kEmpty) {
      ++empty_log_count;
    } else if (log.second == kNotAvailable) {
      ++not_available_log_count;
    } else {
      ++other_log_count;
    }
  }
  if (scrub_) {
    // Record stats for the logs received from debugd.
    // As of today, the total logs are about 211.
    base::UmaHistogramCounts1000(
        "Feedback.ChromeOSApp.GetBigFeedbackLogs.EmptyCount", empty_log_count);
    base::UmaHistogramCounts1000(
        "Feedback.ChromeOSApp.GetBigFeedbackLogs.NotAvailableCount",
        not_available_log_count);
    base::UmaHistogramCounts1000(
        "Feedback.ChromeOSApp.GetBigFeedbackLogs.OtherCount", other_log_count);
  }
  RequestCompleted();
}

void DebugDaemonLogSource::GetLoggedInUsersLogFiles() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // List all logged-in users' profile directories.
  std::vector<base::FilePath> profile_dirs;
  const user_manager::UserList& users =
      user_manager::UserManager::Get()->GetLoggedInUsers();
  for (const user_manager::User* user : users) {
    if (user->username_hash().empty()) {
      continue;
    }

    profile_dirs.emplace_back(
        ash::ProfileHelper::GetProfilePathByUserIdHash(user->username_hash()));
  }

  auto response = std::make_unique<SystemLogsResponse>();
  SystemLogsResponse* response_ptr = response.get();
  base::ThreadPool::PostTaskAndReply(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&ReadUserLogFiles, profile_dirs, response_ptr),
      base::BindOnce(&DebugDaemonLogSource::MergeUserLogFilesResponse,
                     weak_ptr_factory_.GetWeakPtr(), std::move(response)));
}

void DebugDaemonLogSource::MergeUserLogFilesResponse(
    std::unique_ptr<SystemLogsResponse> response) {
  for (auto& pair : *response) {
    response_->emplace(pair.first, std::move(pair.second));
  }

  auto response_to_return = std::make_unique<SystemLogsResponse>();
  std::swap(response_to_return, response_);
  DCHECK(!callback_.is_null());
  std::move(callback_).Run(std::move(response_to_return));
}

void DebugDaemonLogSource::RequestCompleted() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!callback_.is_null());

  --num_pending_requests_;
  if (num_pending_requests_ > 0) {
    return;
  }

  // When all other logs are collected, fetch the user logs, because any errors
  // fetching the other logs is reported in the user logs.
  GetLoggedInUsersLogFiles();
}

}  // namespace system_logs
