// Copyright 2024 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/policy/skyvault/migration_coordinator.h"

#include <memory>
#include <optional>

#include "base/base_paths.h"
#include "base/check_is_test.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/io_task_controller.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/policy/skyvault/drive_skyvault_uploader.h"
#include "chrome/browser/ash/policy/skyvault/histogram_helper.h"
#include "chrome/browser/ash/policy/skyvault/local_files_migration_constants.h"
#include "chrome/browser/ash/policy/skyvault/odfs_skyvault_uploader.h"
#include "chrome/browser/ash/policy/skyvault/policy_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/origin.h"

namespace policy::local_user_files {

namespace {

// Called after `uploader` is fully stopped.
void OnMigrationStopped(std::unique_ptr<MigrationCloudUploader> uploader,
                        base::OnceClosure cb) {
  VLOG(1) << "Local files migration stopped";
  if (cb) {
    std::move(cb).Run();
  }
}

// Returns the file's path relative to MyFiles.
base::FilePath GetPathRelativeToMyFiles(Profile* profile,
                                        const base::FilePath& file_path) {
  base::FilePath my_files_path = GetMyFilesPath(profile);
  base::FilePath rel_path;
  my_files_path.AppendRelativePath(file_path.DirName(), &rel_path);
  return rel_path;
}

bool ErrorCanBeIgnored(MigrationUploadError error) {
  return error == MigrationUploadError::kDeleteFailed ||
         error == MigrationUploadError::kFileNotFound;
}

std::string FormatErrorMessage(MigrationDestination destination,
                               MigrationUploadError error) {
  CHECK(IsCloudDestination(destination));
  switch (error) {
    case MigrationUploadError::kCloudQuotaFull:
      return base::UTF16ToUTF8(base::ReplaceStringPlaceholders(
          l10n_util::GetStringUTF16(
              IDS_POLICY_SKYVAULT_MIGRATION_UPLOAD_ERROR_NO_SPACE),
          l10n_util::GetStringUTF16(
              destination == MigrationDestination::kGoogleDrive
                  ? IDS_OFFICE_CLOUD_PROVIDER_GOOGLE_DRIVE_SHORT
                  : IDS_OFFICE_CLOUD_PROVIDER_ONEDRIVE_SHORT),
          /*offset=*/nullptr));
    case MigrationUploadError::kFileNotFound:
      return l10n_util::GetStringUTF8(
          IDS_POLICY_SKYVAULT_MIGRATION_UPLOAD_ERROR_FILE_NOT_EXIST);
    case MigrationUploadError::kAuthRequired:
      return l10n_util::GetStringUTF8(
          IDS_POLICY_SKYVAULT_MIGRATION_UPLOAD_ERROR_ODFS_SIGN_IN);
    case MigrationUploadError::kNetworkError:
    case MigrationUploadError::kReconnectTimeout:
      return l10n_util::GetStringUTF8(
          IDS_POLICY_SKYVAULT_MIGRATION_UPLOAD_ERROR_NO_INTERNET);
    case MigrationUploadError::kCopyFailed:
    case MigrationUploadError::kCreateFolderFailed:
    case MigrationUploadError::kSyncFailed:
    case MigrationUploadError::kDeleteFailed:
    case MigrationUploadError::kMoveFailed:
    case MigrationUploadError::kCancelled:
    case MigrationUploadError::kUnexpectedError:
    case MigrationUploadError::kServiceUnavailable:
    case MigrationUploadError::kInvalidURL:
      return l10n_util::GetStringUTF8(
          IDS_POLICY_SKYVAULT_MIGRATION_UPLOAD_ERROR_GENERIC);
  }
}

void CloseFile(base::File file) {
  file.Close();
}

base::File CreateOrOpenLogFile(Profile* profile, base::FilePath path) {
  base::File error_log_file_ =
      base::File(path, base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_APPEND);
  if (!error_log_file_.IsValid()) {
    PLOG(ERROR) << "Failed to open migration error log file.";
  }
  return error_log_file_;
}

void LogError(base::File& error_log_file,
              MigrationDestination destination,
              base::FilePath file_path,
              MigrationUploadError error) {
  if (!error_log_file.IsValid()) {
    LOG(ERROR) << "Cannot log error: log file invalid.";
    return;
  }

  std::string log_entry =
      absl::StrFormat("%s - %s\n", file_path.AsUTF8Unsafe(),
                      FormatErrorMessage(destination, error));
  error_log_file.WriteAtCurrentPosAndCheck(base::as_byte_span(log_entry));
}

}  // namespace

MigrationCoordinator::MigrationCoordinator(Profile* profile)
    : profile_(profile) {
  error_log_path_ =
      base::FilePath(kErrorLogFileBasePath).Append(kErrorLogFileName);
}

MigrationCoordinator::~MigrationCoordinator() = default;

void MigrationCoordinator::Run(MigrationDestination destination,
                               std::vector<base::FilePath> files,
                               const std::string& upload_root,
                               MigrationDoneCallback callback) {
  CHECK(!uploader_);

  MigrationDoneCallback wrapped_callback =
      base::BindOnce(&MigrationCoordinator::OnMigrationDone,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback));
  switch (destination) {
    case MigrationDestination::kGoogleDrive:
      uploader_ = std::make_unique<GoogleDriveMigrationUploader>(
          profile_, std::move(files), upload_root, error_log_path_,
          std::move(wrapped_callback));
      break;
    case MigrationDestination::kOneDrive:
      uploader_ = std::make_unique<OneDriveMigrationUploader>(
          profile_, std::move(files), upload_root, error_log_path_,
          std::move(wrapped_callback));
      break;
    case MigrationDestination::kDelete:
    case MigrationDestination::kNotSpecified:
      NOTREACHED() << "Run() should only be called if destination is set to a "
                      "cloud location";
  }
  uploader_->Run();
}

void MigrationCoordinator::Cancel(MigrationStoppedCallback callback) {
  if (uploader_) {
    MigrationCloudUploader* uploader_ptr = uploader_.get();
    uploader_ptr->Cancel(base::BindOnce(&OnMigrationStopped,
                                        std::move(uploader_),
                                        std::move(cancelled_cb_for_testing_)));
  }

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&base::DeleteFile, error_log_path_), std::move(callback));
}

bool MigrationCoordinator::IsRunning() const {
  return uploader_ != nullptr;
}

void MigrationCoordinator::SetCancelledCallbackForTesting(
    base::OnceClosure cb) {
  CHECK_IS_TEST();
  cancelled_cb_for_testing_ = std::move(cb);
}

void MigrationCoordinator::SetErrorLogPathForTesting(
    const base::FilePath& path) {
  CHECK_IS_TEST();
  error_log_path_ = path;
}

void MigrationCoordinator::OnMigrationDone(
    MigrationDoneCallback callback,
    std::map<base::FilePath, MigrationUploadError> errors,
    base::FilePath upload_root_path,
    base::FilePath error_log_path) {
  uploader_.reset();
  std::move(callback).Run(std::move(errors), upload_root_path, error_log_path);
}

MigrationCloudUploader::MigrationCloudUploader(
    Profile* profile,
    std::vector<base::FilePath> files,
    const std::string& upload_root,
    const base::FilePath& error_log_path,
    MigrationDoneCallback callback)
    : profile_(profile),
      files_(std::move(files)),
      upload_root_(upload_root),
      done_callback_(std::move(callback)),
      error_log_path_(error_log_path),
      log_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN})) {}

MigrationCloudUploader::~MigrationCloudUploader() {
  log_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&CloseFile, std::move(error_log_file_)));
}

void MigrationCloudUploader::Run() {
  if (files_.empty()) {
    if (done_callback_) {
      std::move(done_callback_).Run({}, base::FilePath(), base::FilePath());
    }
    return;
  }

  log_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&CreateOrOpenLogFile, profile_, error_log_path_),
      base::BindOnce(&MigrationCloudUploader::OnLogFileReady,
                     weak_ptr_factory_.GetWeakPtr()));
}

OneDriveMigrationUploader::OneDriveMigrationUploader(
    Profile* profile,
    std::vector<base::FilePath> files,
    const std::string& upload_root,
    const base::FilePath& error_log_path,
    MigrationDoneCallback callback)
    : MigrationCloudUploader(profile,
                             std::move(files),
                             upload_root,
                             error_log_path,
                             std::move(callback)) {}

OneDriveMigrationUploader::~OneDriveMigrationUploader() = default;

void OneDriveMigrationUploader::OnLogFileReady(base::File log_file) {
  error_log_file_ = std::move(log_file);
  // TODO(aidazolic): Consider if we can start all jobs at the same time, or we
  // need chunking.
  for (const auto& file_path : files_) {
    base::FilePath relative_path =
        GetPathRelativeToMyFiles(profile_, file_path);
    auto uploader = ash::cloud_upload::OdfsSkyvaultUploader::Upload(
        profile_, file_path, relative_path, upload_root_,
        UploadTrigger::kMigration,
        // No need to show progress updates.
        /*progress_callback=*/base::DoNothing(),
        /*upload_callback=*/
        base::BindOnce(&OneDriveMigrationUploader::OnUploadDone,
                       weak_ptr_factory_.GetWeakPtr(), file_path));
    uploaders_.insert({file_path, std::move(uploader)});
  }
}

void OneDriveMigrationUploader::Cancel(base::OnceClosure callback) {
  cancelled_callback_ = std::move(callback);
  cancelled_ = true;
  // Create a copy of the keys to iterate over. This is necessary because
  // calling Cancel() on the uploader may trigger OnUploadDone(), which
  // modifies the |uploaders_| map, potentially invalidating iterators.
  std::vector<base::FilePath> file_paths;
  for (const auto& uploader : uploaders_) {
    file_paths.push_back(uploader.first);
  }

  for (const auto& path : file_paths) {
    uploaders_[path]->Cancel();
  }
}

void OneDriveMigrationUploader::OnUploadDone(
    const base::FilePath& file_path,
    storage::FileSystemURL url,
    std::optional<MigrationUploadError> error,
    base::FilePath upload_root_path) {
  if (upload_root_path_.empty()) {
    upload_root_path_ = upload_root_path;
  }

  if (!error.has_value()) {
    OnErrorLogged(file_path);
    return;
  }

  SkyVaultMigrationUploadErrorHistogram(MigrationDestination::kOneDrive,
                                        error.value());

  if (!ErrorCanBeIgnored(error.value())) {
    errors_.insert({file_path, error.value()});
  }

  if (error.value() == MigrationUploadError::kNetworkError) {
    // Just retry, uploaders handle waiting for connectivity.
    base::FilePath relative_path =
        GetPathRelativeToMyFiles(profile_, file_path);
    // Safe to replace; original OdfsSkyvaultUploader is destroyed when its
    // Upload method completes.
    uploaders_.insert_or_assign(
        file_path,
        ash::cloud_upload::OdfsSkyvaultUploader::Upload(
            profile_, file_path, relative_path, upload_root_,
            UploadTrigger::kMigration,
            // No need to show progress updates.
            /*progress_callback=*/base::DoNothing(),
            /*upload_callback=*/
            base::BindOnce(&OneDriveMigrationUploader::OnUploadDone,
                           weak_ptr_factory_.GetWeakPtr(), file_path)));
    return;
  }

  if (!error_log_file_.IsValid()) {
    LOG(ERROR) << "Cannot log error: log file invalid";
    OnErrorLogged(file_path);
    return;
  }

  log_task_runner_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&LogError, std::ref(error_log_file_),
                     MigrationDestination::kOneDrive, file_path, error.value()),
      base::BindOnce(&OneDriveMigrationUploader::OnErrorLogged,
                     weak_ptr_factory_.GetWeakPtr(), file_path));
}

void OneDriveMigrationUploader::OnErrorLogged(const base::FilePath& file_path) {
  uploaders_.erase(file_path);

  if (!uploaders_.empty()) {
    // Some files are still being uploaded.
    return;
  }

  if (cancelled_) {
    CHECK(cancelled_callback_);
    std::move(cancelled_callback_).Run();
    return;
  }
  CHECK(done_callback_);
  // TODO(aidazolic): What to do if the log file isn't valid?
  std::move(done_callback_)
      .Run(std::move(errors_), upload_root_path_, error_log_path_);
}

GoogleDriveMigrationUploader::GoogleDriveMigrationUploader(
    Profile* profile,
    std::vector<base::FilePath> files,
    const std::string& upload_root,
    const base::FilePath& error_log_path,
    MigrationDoneCallback callback)
    : MigrationCloudUploader(profile,
                             std::move(files),
                             upload_root,
                             error_log_path,
                             std::move(callback)) {}

GoogleDriveMigrationUploader::~GoogleDriveMigrationUploader() = default;

void GoogleDriveMigrationUploader::OnLogFileReady(base::File log_file) {
  error_log_file_ = std::move(log_file);
  // TODO(aidazolic): Consider if we can start all jobs at the same time, or we
  // need chunking.
  for (const auto& file_path : files_) {
    base::FilePath target_path = GetPathRelativeToMyFiles(profile_, file_path);
    std::unique_ptr<DriveSkyvaultUploader> uploader =
        std::make_unique<DriveSkyvaultUploader>(
            profile_, file_path, target_path, upload_root_,
            base::BindOnce(&GoogleDriveMigrationUploader::OnUploadDone,
                           weak_ptr_factory_.GetWeakPtr(), file_path));

    auto uploader_ptr = uploader.get();
    uploaders_.insert({file_path, std::move(uploader)});
    uploader_ptr->Run();
  }
}

void GoogleDriveMigrationUploader::Cancel(base::OnceClosure callback) {
  cancelled_callback_ = std::move(callback);
  cancelled_ = true;
  // Create a copy of the keys to iterate over. This is necessary because
  // calling Cancel() on the uploader may trigger OnUploadDone(), which
  // modifies the |uploaders_| map, potentially invalidating iterators.
  std::vector<base::FilePath> file_paths;
  for (const auto& uploader : uploaders_) {
    file_paths.push_back(uploader.first);
  }

  for (const auto& path : file_paths) {
    uploaders_[path]->Cancel();
  }
}

void GoogleDriveMigrationUploader::OnUploadDone(
    const base::FilePath& file_path,
    std::optional<MigrationUploadError> error,
    base::FilePath upload_root_path) {
  // Record the destination path the first time we receive it.
  if (upload_root_path_.empty()) {
    upload_root_path_ = upload_root_path;
  }

  if (!error.has_value()) {
    OnErrorLogged(file_path);
    return;
  }

  SkyVaultMigrationUploadErrorHistogram(MigrationDestination::kGoogleDrive,
                                        error.value());

  if (!ErrorCanBeIgnored(error.value())) {
    errors_.insert({file_path, error.value()});
  }

  if (error.value() == MigrationUploadError::kNetworkError) {
    // Just retry, uploaders handle waiting for connectivity.
    base::FilePath target_path = GetPathRelativeToMyFiles(profile_, file_path);
    std::unique_ptr<DriveSkyvaultUploader> uploader =
        std::make_unique<DriveSkyvaultUploader>(
            profile_, file_path, target_path, upload_root_,
            base::BindOnce(&GoogleDriveMigrationUploader::OnUploadDone,
                           weak_ptr_factory_.GetWeakPtr(), file_path));

    auto uploader_ptr = uploader.get();
    // Safe to replace; original DriveSkyvaultUploader is destroyed when its
    // unique_ptr is removed from the map.
    uploaders_.insert_or_assign(file_path, std::move(uploader));
    uploader_ptr->Run();
    return;
  }

  if (!error_log_file_.IsValid()) {
    LOG(ERROR) << "Cannot log error: log file invalid";
    OnErrorLogged(file_path);
    return;
  }

  log_task_runner_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&LogError, std::ref(error_log_file_),
                     MigrationDestination::kGoogleDrive, file_path,
                     error.value()),
      base::BindOnce(&GoogleDriveMigrationUploader::OnErrorLogged,
                     weak_ptr_factory_.GetWeakPtr(), file_path));
}

void GoogleDriveMigrationUploader::OnErrorLogged(
    const base::FilePath& file_path) {
  uploaders_.erase(file_path);

  if (!uploaders_.empty()) {
    // Some files are still being uploaded.
    return;
  }

  if (cancelled_) {
    CHECK(cancelled_callback_);
    std::move(cancelled_callback_).Run();
    return;
  }
  CHECK(done_callback_);
  // TODO(aidazolic): What to do if the log file isn't valid?
  std::move(done_callback_)
      .Run(std::move(errors_), upload_root_path_, error_log_path_);
}

}  // namespace policy::local_user_files
