// 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 "components/reporting/util/file.h"

#include <algorithm>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/containers/span.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/types/expected.h"
#include "components/reporting/util/reporting_errors.h"

namespace reporting {

bool DeleteFileWarnIfFailed(const base::FilePath& path) {
  const auto delete_result = base::DeleteFile(path);
  if (!delete_result) {
    LOG(WARNING) << "Failed to delete " << path.MaybeAsASCII();
  }
  return delete_result;
}

bool DeleteFilesWarnIfFailed(
    base::FileEnumerator& dir_enum,
    base::RepeatingCallback<bool(const base::FilePath&)> pred) {
  std::vector<base::FilePath> files_to_delete;
  for (auto full_name = dir_enum.Next(); !full_name.empty();
       full_name = dir_enum.Next()) {
    if (pred.Run(full_name)) {
      files_to_delete.push_back(std::move(full_name));
    }
  }

  // Starting from deeper paths so that directories are always emptied first if
  // the files there are to be deleted. This can be done by deleting the file
  // with the longest full paths first.
  std::sort(files_to_delete.begin(), files_to_delete.end(),
            [](const base::FilePath& fp0, const base::FilePath& fp1) {
              // Use size of the file path string is sufficient. Semantically it
              // is better to use the number of components in a file path
              // (GetComponents().size()), but this is more efficient.
              return fp0.value().size() > fp1.value().size();
            });
  bool success = true;
  for (const auto& file_to_delete : files_to_delete) {
    if (!DeleteFileWarnIfFailed(file_to_delete)) {
      success = false;
    }
  }
  return success;
}

bool DeleteFilesWarnIfFailed(
    base::FileEnumerator&& dir_enum,
    base::RepeatingCallback<bool(const base::FilePath&)> pred) {
  return DeleteFilesWarnIfFailed(dir_enum, pred);
}

StatusOr<std::string> MaybeReadFile(const base::FilePath& file_path,
                                    int64_t offset) {
  base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
  if (!file.IsValid()) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::FAILED_TO_OPEN_FILE,
                                  DataLossErrorReason::MAX_VALUE);
    return base::unexpected(Status(
        error::NOT_FOUND, base::StrCat({"Could not open health data file ",
                                        file_path.MaybeAsASCII()})));
  }

  base::File::Info file_info;
  if (!file.GetInfo(&file_info) || file_info.size - offset < 0) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::FAILED_TO_READ_FILE_INFO,
                                  DataLossErrorReason::MAX_VALUE);
    return base::unexpected(
        Status(error::DATA_LOSS, base::StrCat({"Failed to read data file info ",
                                               file_path.MaybeAsASCII()})));
  }

  std::string result;
  result.resize(file_info.size - offset);
  if (file.Read(offset, base::as_writable_byte_span(result)) != result.size()) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::FAILED_TO_READ_FILE,
                                  DataLossErrorReason::MAX_VALUE);
    return base::unexpected(Status(
        error::DATA_LOSS,
        base::StrCat({"Failed to read data file ", file_path.MaybeAsASCII()})));
  }

  return result;
}

Status AppendLine(const base::FilePath& file_path,
                  const std::string_view& data) {
  base::File file(file_path,
                  base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_APPEND);
  if (!file.IsValid()) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::FAILED_TO_OPEN_FILE,
                                  DataLossErrorReason::MAX_VALUE);
    return Status(error::NOT_FOUND,
                  base::StrCat({"Could not open health data file ",
                                file_path.MaybeAsASCII()}));
  }

  const std::string line = base::StrCat({data, "\n"});
  if (const std::optional<size_t> bytes_written =
          file.Write(0, base::as_byte_span(line));
      bytes_written != line.size()) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::FAILED_TO_WRITE_FILE,
                                  DataLossErrorReason::MAX_VALUE);
    const int write_count = bytes_written.value_or(-1);
    return Status(error::DATA_LOSS,
                  base::StrCat({"Failed to write health data file ",
                                file_path.MaybeAsASCII(), " write count=",
                                base::NumberToString(write_count)}));
  }
  return Status::StatusOK();
}

StatusOr<uint32_t> RemoveAndTruncateLine(const base::FilePath& file_path,
                                         uint32_t pos) {
  StatusOr<std::string> status_or = MaybeReadFile(file_path, pos);
  if (!status_or.has_value()) {
    return base::unexpected(std::move(status_or).error());
  }
  std::string content = status_or.value();
  uint32_t offset = 0;
  // Search for next new line after pos.
  while (offset < content.length()) {
    if (content.at(offset++) == '\n') {
      break;
    }
  }

  // Check if the last line was removed.
  if (offset >= content.length()) {
    content = "";
  } else {
    content = content.substr(offset);
  }

  Status status = MaybeWriteFile(file_path, content);
  if (!status.ok()) {
    return base::unexpected(std::move(status));
  }
  return pos + offset;
}

Status MaybeWriteFile(const base::FilePath& file_path,
                      const std::string_view& data) {
  base::File file(file_path,
                  base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
  if (!file.IsValid()) {
    return Status(error::NOT_FOUND, base::StrCat({"Could not open data file ",
                                                  file_path.MaybeAsASCII()}));
  }

  if (const std::optional<size_t> bytes_written =
          file.Write(0, base::as_byte_span(data));
      bytes_written != data.size()) {
    base::UmaHistogramEnumeration(reporting::kUmaDataLossErrorReason,
                                  DataLossErrorReason::FAILED_TO_WRITE_FILE,
                                  DataLossErrorReason::MAX_VALUE);
    const int write_count = bytes_written.value_or(-1);
    return Status(
        error::DATA_LOSS,
        base::StrCat({"Failed to write data file ", file_path.MaybeAsASCII(),
                      " write count=", base::NumberToString(write_count)}));
  }

  return Status::StatusOK();
}
}  // namespace reporting
