// Copyright 2022 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/web_applications/web_app_logging.h"

#include <string>
#include <string_view>

#include "base/auto_reset.h"
#include "base/barrier_closure.h"
#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/containers/circular_deque.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/i18n/time_formatting.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/json_writer.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/types/expected_macros.h"
#include "chrome/browser/web_applications/file_utils_wrapper.h"
#include "chrome/browser/web_applications/model/web_app_icon_types.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/web_contents/web_app_url_loader.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browser_thread.h"
#include "net/http/http_status_code.h"

namespace web_app {
namespace {
// Heuristic from looking at a number of log entries and averaging their byte
// size.
static constexpr int kEstimatedBytesPerLogEntry = 1 * 1024;  // 2KB
// The file size should be large enough that it will not need to rotate often,
// but small enough that it will not get too expensive to re-serialize every
// time the log is flushed.
static constexpr int kMaxLogFileSizeBytes = 2 * 1024 * 1024;  // 2MB
static constexpr base::TimeDelta kLogWriteDelay = base::Seconds(5);
// When rotating log files, there can be multiple files that rotate on the same
// date. To create a unique file name, we add an index to the end of the file
// name. This is the maximum index that will be used.
static constexpr int kMaxRotatedLogFileIndex = 100;

int g_max_log_file_size_bytes = kMaxLogFileSizeBytes;

bool IsEmptyIconBitmapsForIconUrl(const IconsMap& icons_map,
                                  const GURL& icon_url) {
  IconsMap::const_iterator iter = icons_map.find(icon_url);
  if (iter == icons_map.end())
    return true;

  const std::vector<SkBitmap>& icon_bitmaps = iter->second;
  if (icon_bitmaps.empty())
    return true;

  for (const SkBitmap& icon_bitmap : icon_bitmaps) {
    if (!icon_bitmap.isNull() && !icon_bitmap.drawsNothing())
      return false;
  }

  return true;
}

void WritePersistableLogBlocking(scoped_refptr<FileUtilsWrapper> utils,
                                 const base::FilePath& file_path,
                                 base::Value error_log) {
  if (!utils->CreateDirectory(file_path.DirName())) {
    DLOG(ERROR) << "Could not create directory: " << file_path.DirName();
    return;
  }
  if (!utils->WriteFile(file_path,
                        base::as_byte_span(error_log.DebugString()))) {
    DLOG(ERROR) << "Could not write log file: " << file_path;
  }
}

std::optional<base::ListValue> ReadLogFileBlocking(
    scoped_refptr<FileUtilsWrapper> utils,
    const base::FilePath& log_file) {
  if (!utils->PathExists(log_file)) {
    return std::nullopt;
  }
  std::string contents;
  if (!utils->ReadFileToString(log_file, &contents)) {
    return std::nullopt;
  }
  return base::JSONReader::ReadList(
      contents, base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS);
}

void RotateLogFileBlocking(scoped_refptr<FileUtilsWrapper> utils,
                           const base::FilePath& current_log_file,
                           const base::Clock* clock) {
  if (!utils->PathExists(current_log_file)) {
    DLOG(ERROR) << "Current log file does not exist: " << current_log_file;
    return;
  }
  std::optional<base::FilePath> rotated_filename;
  base::Time::Exploded exploded;
  clock->Now().LocalExplode(&exploded);
  std::string date_str = base::StringPrintf(
      "%04d-%02d-%02d", exploded.year, exploded.month, exploded.day_of_month);
  for (int free_index = -1; free_index < kMaxRotatedLogFileIndex;
       ++free_index) {
    std::string suffix = date_str;
    if (free_index >= 0) {
      base::StrAppend(&suffix, {".", base::NumberToString(free_index)});
    }
    base::FilePath candidate_filename =
        current_log_file.InsertBeforeExtensionASCII(suffix);
    if (!utils->PathExists(candidate_filename)) {
      rotated_filename = candidate_filename;
      break;
    }
  }
  if (!rotated_filename.has_value()) {
    DLOG(ERROR) << "Could not find a free index for rotated log file.";
    return;
  }
  if (!utils->Move(current_log_file, *rotated_filename)) {
    LOG(ERROR) << "Could not move log file: " << current_log_file.value()
               << " to " << rotated_filename.value();
  }
}

}  // namespace

// static
base::FilePath PersistableLog::GetLogPath(Profile* profile,
                                          std::string_view log_filename) {
  return GetWebAppsRootDirectory(profile).AppendASCII("Logs").AppendASCII(
      log_filename);
}
// static
int PersistableLog::GetMaxInMemoryLogEntries() {
  static constexpr int kMaxInMemoryLogEntriesWithDebugInfoEnabled = 1000;
  static constexpr int kMaxInMemoryLogEntriesWithDebugInfoDisabled = 20;
  return base::FeatureList::IsEnabled(features::kRecordWebAppDebugInfo)
             ? kMaxInMemoryLogEntriesWithDebugInfoEnabled
             : kMaxInMemoryLogEntriesWithDebugInfoDisabled;
}

// static
PersistableLogMode PersistableLog::GetMode() {
  return base::FeatureList::IsEnabled(features::kRecordWebAppDebugInfo)
             ? PersistableLogMode::kPersistToDisk
             : PersistableLogMode::kInMemory;
}

// static
base::AutoReset<int> PersistableLog::SetMaxLogFileSizeBytesForTesting(
    int size) {
  CHECK_IS_TEST();
  return base::AutoReset<int>(&g_max_log_file_size_bytes, size);
}

std::unique_ptr<PersistableLog> PersistableLog::Create(
    const base::FilePath& log_file,
    PersistableLogMode mode,
    int max_log_entries_in_memory,
    scoped_refptr<FileUtilsWrapper> file_utils) {
  return base::WrapUnique(new PersistableLog(
      log_file, mode, max_log_entries_in_memory, std::move(file_utils),
      base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN}),
      base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
      base::DefaultClock::GetInstance()));
}

std::unique_ptr<PersistableLog> PersistableLog::CreateForTesting(
    const base::FilePath& log_file,
    PersistableLogMode mode,
    int max_log_entries_in_memory,
    scoped_refptr<FileUtilsWrapper> file_utils,
    scoped_refptr<base::SequencedTaskRunner> log_writing_task_runner,
    scoped_refptr<base::SequencedTaskRunner> log_deletion_task_runner,
    const base::Clock* clock) {
  return base::WrapUnique(new PersistableLog(
      log_file, mode, max_log_entries_in_memory, std::move(file_utils),
      std::move(log_writing_task_runner), std::move(log_deletion_task_runner),
      clock));
}

PersistableLog::PersistableLog(
    const base::FilePath& log_file,
    PersistableLogMode mode,
    int max_log_entries_in_memory,
    scoped_refptr<FileUtilsWrapper> file_utils,
    scoped_refptr<base::SequencedTaskRunner> log_writing_task_runner,
    scoped_refptr<base::SequencedTaskRunner> log_deletion_task_runner,
    const base::Clock* clock)
    : log_file_(log_file),
      mode_(mode),
      file_utils_(std::move(file_utils)),
      max_log_entries_in_memory_(max_log_entries_in_memory),
      log_writing_task_runner_(std::move(log_writing_task_runner)),
      log_deletion_task_runner_(std::move(log_deletion_task_runner)),
      clock_(CHECK_DEREF(clock)),
      log_write_timer_(FROM_HERE,
                       kLogWriteDelay,
                       this,
                       &PersistableLog::MaybeWriteCurrentLog) {
  CHECK(!log_file_.empty());
  CHECK(file_utils_);
  CHECK(log_file_.IsAbsolute());
  CHECK(!log_file_.Extension().empty());

  switch (mode) {
    case PersistableLogMode::kInMemory:
      // For in-memory, we only do one file-related task here to delete old
      // logs.
      // TODO(crbug.com/40224498): Clean up logs older than X days on startup.
      log_deletion_task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(
              base::IgnoreResult(&FileUtilsWrapper::DeleteFileRecursively),
              file_utils_, log_file_.DirName()));
      // Signal the load complete event so that Append() can add to the
      // in-memory log.
      on_load_complete_.Signal();
      return;
    case PersistableLogMode::kPersistToDisk:
      log_writing_task_runner_->PostTaskAndReplyWithResult(
          FROM_HERE,
          base::BindOnce(&ReadLogFileBlocking, file_utils_, log_file_),
          base::BindOnce(&PersistableLog::OnLatestLogLoaded,
                         weak_ptr_factory_.GetWeakPtr()));
      break;
  }
}

PersistableLog::~PersistableLog() {
  MaybeWriteCurrentLog();
}

void PersistableLog::Append(base::DictValue object) {
  if (!object.contains("timestamp")) {
    object.Set("timestamp", base::TimeFormatAsIso8601(clock_->Now()));
  }
#if DCHECK_IS_ON()
  // This is wrapped with DCHECK_IS_ON() to prevent calling DebugString() in
  // production builds.
  DVLOG(1) << (on_load_complete_.is_signaled() ? "" : "[LOADING] ")
           << log_file_.BaseName() << ": " << object.DebugString();
#endif
  if (!on_load_complete_.is_signaled()) {
    on_load_complete_.Post(
        FROM_HERE,
        base::BindOnce(&PersistableLog::Append, weak_ptr_factory_.GetWeakPtr(),
                       std::move(object)));
    return;
  }
  log_.push_front(object.Clone());
  if (base::saturated_cast<int>(log_.size()) > max_log_entries_in_memory_) {
    log_.resize(max_log_entries_in_memory_);
  }
  current_log_for_disk_.Append(std::move(object));
  if (mode_ == PersistableLogMode::kPersistToDisk) {
    log_write_timer_.Reset();
    // Technically we can log frequently for a while and never write to disk, as
    // the timer won't trigger. To mitigate, flush if we reach 2x the max log
    // size.
    if (base::saturated_cast<int>(current_log_for_disk_.size()) *
            kEstimatedBytesPerLogEntry * 2 >=
        g_max_log_file_size_bytes) {
      MaybeWriteCurrentLog();
    }
  }
}

void PersistableLog::AppendValue(base::Value object) {
  Append(base::DictValue().Set("value", std::move(object)));
}

const base::circular_deque<base::DictValue>& PersistableLog::GetEntries()
    const {
  return log_;
}

base::ListValue PersistableLog::CloneToList() const {
  base::ListValue log_entries = base::ListValue::with_capacity(log_.size());
  for (const base::DictValue& entry : log_) {
    log_entries.Append(entry.Clone());
  }
  return log_entries;
}

void PersistableLog::OnLatestLogLoaded(
    std::optional<base::ListValue> loaded_log) {
  if (loaded_log.has_value()) {
    current_log_for_disk_ = loaded_log->Clone();
    // Note: Items are added to the front as the in-memory log order is from
    // newest to oldest.
    log_.reserve(current_log_for_disk_.size());
    for (base::Value& value : current_log_for_disk_) {
      // Ignore non-dict entries, which are not expected.
      if (!value.is_dict()) {
        continue;
      }
      log_.push_front(value.GetDict().Clone());
    }
  }
  on_load_complete_.Signal();
}

void PersistableLog::MaybeWriteCurrentLog() {
  if (mode_ != PersistableLogMode::kPersistToDisk) {
    return;
  }

  // The log is written, in full, every time this method is called. This is a
  // tradeoff between using an easy-to-read and easy-to-write format like JSON,
  // which requires re-serialization fully every write (as you cannot simply
  // easily append with our built-in methods), and the cost of rewriting the
  // whole file. Since this is only used for debugging for users who have
  // enabled the debug info flag, the costs here seem acceptable, especially
  // since the total log size is limited, and we only write 5 seconds after the
  // last log entry is added.
  base::OnceClosure log_write_task =
      base::BindOnce(&WritePersistableLogBlocking, file_utils_, log_file_,
                     base::Value(current_log_for_disk_.Clone()));
  // Possibly do a log rotation.
  if (current_disk_log_entry_count_ * kEstimatedBytesPerLogEntry >
      g_max_log_file_size_bytes) {
    log_write_task = base::BindOnce(&RotateLogFileBlocking, file_utils_,
                                    log_file_, base::Unretained(&clock_.get()))
                         .Then(std::move(log_write_task));
    current_log_for_disk_.clear();
  }

  log_writing_task_runner_->PostTask(FROM_HERE, std::move(log_write_task));
  current_disk_log_entry_count_ = current_log_for_disk_.size();
}

base::DictValue LogDownloadedIconsErrors(
    IconsDownloadedResult icons_downloaded_result,
    const IconsMap& icons_map,
    const DownloadedIconsHttpResults& icons_http_results) {
  base::DictValue icon_errors;
  {
    // Reports errors only, omits successful entries.
    base::ListValue icons_http_errors;

    for (const auto& url_and_http_code : icons_http_results) {
      const GURL& icon_url = url_and_http_code.first.url;
      int http_status_code = url_and_http_code.second;
      std::string_view http_code_desc =
          net::GetHttpReasonPhrase(http_status_code);

      // If the SkBitmap for`icon_url` is missing in `icons_map` then we report
      // this miss as an error, even for net::HttpStatusCode::HTTP_OK.
      if (IsEmptyIconBitmapsForIconUrl(icons_map, icon_url)) {
        base::DictValue icon_http_error;

        icon_http_error.Set("icon_url", icon_url.spec());
        icon_http_error.Set("icon_size",
                            url_and_http_code.first.size.ToString());
        icon_http_error.Set("http_status_code", http_status_code);
        icon_http_error.Set("http_code_desc", http_code_desc);

        icons_http_errors.Append(std::move(icon_http_error));
      }
    }

    if (icons_downloaded_result != IconsDownloadedResult::kCompleted ||
        !icons_http_errors.empty()) {
      icon_errors.Set("icons_downloaded_result",
                      IconsDownloadedResultToString(icons_downloaded_result));
    }

    if (!icons_http_errors.empty())
      icon_errors.Set("icons_http_results", std::move(icons_http_errors));
  }

  return icon_errors;
}

}  // namespace web_app
