// 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/drive_skyvault_uploader.h"

#include <optional>

#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/task/thread_pool.h"
#include "base/timer/timer.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/drive/drive_integration_service_factory.h"
#include "chrome/browser/ash/drive/file_system_util.h"
#include "chrome/browser/ash/file_manager/copy_or_move_io_task.h"
#include "chrome/browser/ash/file_manager/delete_io_task.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/io_task.h"
#include "chrome/browser/ash/file_manager/office_file_tasks.h"
#include "chrome/browser/ash/file_manager/volume_manager.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/policy_utils.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"
#include "chromeos/ash/components/drivefs/drivefs_host.h"
#include "components/drive/file_errors.h"

using storage::FileSystemURL;

namespace policy::local_user_files {
namespace {
// Creates a directory at `dir_path`, if it doesn't already exist. Returns true
// if directory exists or is created successfully.
bool CreateDirectoryIfNeeded(const base::FilePath& dir_path) {
  base::File::Error error = base::File::FILE_OK;
  if (base::DirectoryExists(dir_path)) {
    return true;
  }
  if (!base::CreateDirectoryAndGetError(dir_path, &error)) {
    PLOG(ERROR) << "Failed to create directory: "
                << base::File::ErrorToString(error);
    return false;
  }
  return true;
}
}  // namespace

DriveSkyvaultUploader::DriveSkyvaultUploader(
    Profile* profile,
    const base::FilePath& file_path,
    const base::FilePath& relative_source_path,
    const std::string& upload_root,
    UploadCallback callback)
    : profile_(profile),
      file_system_context_(
          file_manager::util::GetFileManagerFileSystemContext(profile)),
      drive_integration_service_(
          drive::DriveIntegrationServiceFactory::FindForProfile(profile)),
      source_url_(file_system_context_->CreateCrackedFileSystemURL(
          blink::StorageKey(),
          storage::kFileSystemTypeLocal,
          file_path)),
      relative_source_path_(relative_source_path),
      upload_root_(upload_root),
      callback_(std::move(callback)) {}

DriveSkyvaultUploader::~DriveSkyvaultUploader() = default;

void DriveSkyvaultUploader::Run() {
  DCHECK(callback_);

  if (cancelled_) {
    OnEndCopy(MigrationUploadError::kCancelled);
    return;
  }

  if (!profile_) {
    LOG(ERROR) << "No profile";
    OnEndCopy(MigrationUploadError::kUnexpectedError);
    return;
  }

  file_manager::VolumeManager* volume_manager =
      file_manager::VolumeManager::Get(profile_);
  if (!volume_manager) {
    LOG(ERROR) << "No volume manager";
    OnEndCopy(MigrationUploadError::kUnexpectedError);
    return;
  }
  io_task_controller_ = volume_manager->io_task_controller();
  if (!io_task_controller_) {
    LOG(ERROR) << "No task_controller";
    OnEndCopy(MigrationUploadError::kUnexpectedError);
    return;
  }

  if (!drive_integration_service_) {
    LOG(ERROR) << "No Drive integration service";
    OnEndCopy(MigrationUploadError::kServiceUnavailable);
    return;
  }

  // Observe Drive updates.
  drive_observation_.Observe(drive_integration_service_);

  if (!drive_integration_service_->is_enabled()) {
    // Drive is completely disabled for this profile.
    LOG(ERROR) << "Drive integration service isn't available";
    OnEndCopy(MigrationUploadError::kServiceUnavailable);
    return;
  }

  auto drive_status = drive::util::GetDriveConnectionStatus(profile_);
  waiting_for_connection_ =
      drive_status != drive::util::ConnectionStatus::kConnected;
  SkyVaultMigrationWaitForConnectionHistogram(
      MigrationDestination::kGoogleDrive, waiting_for_connection_);
  if (waiting_for_connection_) {
    LOG(ERROR) << "Waiting for connection to Drive";
    connection_wait_start_time_ = base::Time::Now();

    reconnection_timer_.Start(
        FROM_HERE, kReconnectionTimeout,
        base::BindOnce(&DriveSkyvaultUploader::OnReconnectionTimeout,
                       weak_ptr_factory_.GetWeakPtr()));
    return;
  }

  // Observe IO tasks updates.
  io_task_controller_observer_.Observe(io_task_controller_);

  drivefs::DriveFsHost::Observer::Observe(
      drive_integration_service_->GetDriveFsHost());

  if (!drive_integration_service_->IsMounted()) {
    LOG(ERROR) << "Google Drive is not mounted";
    OnEndCopy(MigrationUploadError::kServiceUnavailable);
    return;
  }

  upload_root_path_ = drive_integration_service_->GetMountPointPath()
                          .AppendASCII("root")
                          .AppendASCII(upload_root_);
  auto destination_folder_path =
      upload_root_path_.Append(relative_source_path_);
  // Copy will fail if the full path doesn't already exist in drive, so first
  // create the destination folder if needed.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&CreateDirectoryIfNeeded, destination_folder_path),
      base::BindOnce(&DriveSkyvaultUploader::CreateCopyIOTask,
                     weak_ptr_factory_.GetWeakPtr(), destination_folder_path));
}

void DriveSkyvaultUploader::Cancel() {
  cancelled_ = true;
  // If delete task has started, copy must have finished.
  if (observed_delete_task_id_.has_value()) {
    io_task_controller_->Cancel(observed_delete_task_id_.value());
  } else if (observed_copy_task_id_.has_value()) {
    io_task_controller_->Cancel(observed_copy_task_id_.value());
  }
}

void DriveSkyvaultUploader::CreateCopyIOTask(
    const base::FilePath& destination_folder_path,
    bool created) {
  if (observed_copy_task_id_) {
    NOTREACHED()
        << "The Copy IOTask was already triggered. Case should not be reached.";
  }

  if (cancelled_) {
    OnEndCopy(MigrationUploadError::kCancelled);
    return;
  }

  if (!created) {
    OnEndCopy(MigrationUploadError::kCreateFolderFailed);
    return;
  }

  // Destination url.
  FileSystemURL destination_folder_url =
      ash::cloud_upload::FilePathToFileSystemURL(profile_, file_system_context_,
                                                 destination_folder_path);
  if (!destination_folder_url.is_valid()) {
    LOG(ERROR) << "Unable to generate destination folder Drive URL";
    OnEndCopy(MigrationUploadError::kServiceUnavailable);
    return;
  }

  std::vector<FileSystemURL> source_urls{source_url_};
  // Always use a copy task. Will convert to a move upon success.
  std::unique_ptr<file_manager::io_task::IOTask> copy_task =
      std::make_unique<file_manager::io_task::CopyOrMoveIOTask>(
          file_manager::io_task::OperationType::kCopy, std::move(source_urls),
          std::move(destination_folder_url), profile_, file_system_context_,
          /*show_notification=*/false);

  observed_copy_task_id_ = io_task_controller_->Add(std::move(copy_task));
}

void DriveSkyvaultUploader::SetFailDeleteForTesting(bool fail) {
  CHECK_IS_TEST();
  fail_delete_for_testing_ = fail;
}

void DriveSkyvaultUploader::OnEndCopy(
    std::optional<MigrationUploadError> error) {
  if (copy_ended_) {
    // Prevent loops in case Copy IO task and Drive sync fail separately.
    return;
  }
  copy_ended_ = true;
  CHECK(!error_);
  error_ = error;

  // If destination file doesn't exist, no delete is required.
  base::FilePath rel_path;
  bool destination_file_exists =
      !observed_absolute_dest_path_.empty() &&
      drive_integration_service_->GetRelativeDrivePath(
          observed_absolute_dest_path_, &rel_path);
  if (!destination_file_exists) {
    OnEndUpload();
    return;
  }

  if (cancelled_) {
    error_ = error_.value_or(MigrationUploadError::kCancelled);
    OnEndUpload();
    return;
  }

  if (observed_delete_task_id_) {
    NOTREACHED() << "The delete IOTask was already triggered. Case should not "
                    "be reached.";
  }

  std::vector<FileSystemURL> file_urls;
  if (!error.has_value()) {
    // If copy to Drive was successful, delete source file to convert the upload
    // to a move to Drive.
    file_urls.push_back(source_url_);
  } else {
    // If copy to Drive was unsuccessful, delete destination file to undo the
    // copy to Drive.
    FileSystemURL dest_url = ash::cloud_upload::FilePathToFileSystemURL(
        profile_, file_system_context_, observed_absolute_dest_path_);
    file_urls.push_back(dest_url);
  }

  std::unique_ptr<file_manager::io_task::IOTask> task =
      std::make_unique<file_manager::io_task::DeleteIOTask>(
          std::move(file_urls), file_system_context_,
          /*show_notification=*/false);
  observed_delete_task_id_ = io_task_controller_->Add(std::move(task));
}

void DriveSkyvaultUploader::OnEndUpload() {
  observed_relative_drive_path_.clear();
  SkyVaultDeleteErrorHistogram(UploadTrigger::kMigration,
                               MigrationDestination::kGoogleDrive,
                               error_ == MigrationUploadError::kDeleteFailed);
  std::move(callback_).Run(error_, upload_root_path_);
}

void DriveSkyvaultUploader::OnIOTaskStatus(
    const file_manager::io_task::ProgressStatus& status) {
  if (status.task_id == observed_copy_task_id_) {
    OnCopyStatus(status);
    return;
  }
  if (status.task_id == observed_delete_task_id_) {
    OnDeleteStatus(status);
    return;
  }
}

void DriveSkyvaultUploader::OnCopyStatus(
    const file_manager::io_task::ProgressStatus& status) {
  switch (status.state) {
    case file_manager::io_task::State::kScanning:
    case file_manager::io_task::State::kQueued:
    case file_manager::io_task::State::kPaused:
      return;
    case file_manager::io_task::State::kInProgress:
      if (observed_relative_drive_path_.empty() && !status.outputs.empty()) {
        // It's always one file.
        DCHECK_EQ(status.sources.size(), 1u);
        DCHECK_EQ(status.outputs.size(), 1u);

        if (!drive_integration_service_) {
          LOG(ERROR) << "No Drive integration service";
          OnEndCopy(MigrationUploadError::kServiceUnavailable);
          return;
        }

        // Get the output path from the IOTaskController's ProgressStatus. The
        // destination file name is not known in advance, given that it's
        // generated from the IOTaskController which resolves potential name
        // clashes.
        observed_absolute_dest_path_ = status.outputs[0].url.path();
        drive_integration_service_->GetRelativeDrivePath(
            observed_absolute_dest_path_, &observed_relative_drive_path_);
      }
      return;
    case file_manager::io_task::State::kSuccess:
      DCHECK_EQ(status.outputs.size(), 1u);
      return;
    case file_manager::io_task::State::kCancelled:
      OnEndCopy(MigrationUploadError::kCancelled);
      return;
    case file_manager::io_task::State::kError:
      ProcessCopyError(status);
      return;
    case file_manager::io_task::State::kNeedPassword:
      NOTREACHED() << "Encrypted file should not need password to be copied or "
                      "moved. Case should not be reached.";
  }
}

void DriveSkyvaultUploader::ProcessCopyError(
    const file_manager::io_task::ProgressStatus& status) {
  // It's always one file.
  DCHECK_EQ(status.sources.size(), 1u);
  DCHECK_EQ(status.outputs.size(), 1u);
  DCHECK_EQ(status.state, file_manager::io_task::State::kError);

  base::File::Error error =
      status.outputs.front().error.value_or(base::File::FILE_ERROR_FAILED);
  MigrationUploadError upload_error = MigrationUploadError::kCopyFailed;

  switch (error) {
    case base::File::FILE_ERROR_NOT_FOUND:
      upload_error = MigrationUploadError::kFileNotFound;
      break;
    case base::File::FILE_ERROR_NO_SPACE:
      upload_error = MigrationUploadError::kCloudQuotaFull;
      break;
    default:
      break;
  }

  OnEndCopy(upload_error);
}

void DriveSkyvaultUploader::OnDeleteStatus(
    const file_manager::io_task::ProgressStatus& status) {
  switch (status.state) {
    case file_manager::io_task::State::kCancelled:
      // Don't override errors occurred during the copy.
      error_ = error_.value_or(MigrationUploadError::kCancelled);
      break;
    case file_manager::io_task::State::kError:
      // Don't override errors occurred during the copy.
      error_ = error_.value_or(MigrationUploadError::kDeleteFailed);
      break;
    case file_manager::io_task::State::kSuccess:
      break;
    default:
      return;
  }

  if (fail_delete_for_testing_) {
    CHECK_IS_TEST();
    if (!error_) {
      error_ = MigrationUploadError::kDeleteFailed;
    }
  }

  OnEndUpload();
}

void DriveSkyvaultUploader::OnUnmounted() {}

void DriveSkyvaultUploader::ImmediatelyUploadDone(drive::FileError error) {
  LOG_IF(ERROR, error != drive::FileError::FILE_ERROR_OK)
      << "ImmediatelyUpload failed with status: " << error;
}

void DriveSkyvaultUploader::OnSyncingStatusUpdate(
    const drivefs::mojom::SyncingStatus& syncing_status) {
  for (const auto& item : syncing_status.item_events) {
    if (base::FilePath(item->path) != observed_relative_drive_path_) {
      continue;
    }
    if (item->state == drivefs::mojom::ItemEvent::State::kCancelledAndDeleted) {
      continue;
    }
    switch (item->state) {
      case drivefs::mojom::ItemEvent::State::kQueued: {
        // Tell Drive to upload the file now. If successful, we will receive a
        // kInProgress or kCompleted event sooner. If this fails, we ignore it.
        // The file will get uploaded eventually.
        drive_integration_service_->ImmediatelyUpload(
            observed_relative_drive_path_,
            base::BindOnce(&DriveSkyvaultUploader::ImmediatelyUploadDone,
                           weak_ptr_factory_.GetWeakPtr()));
        return;
      }
      case drivefs::mojom::ItemEvent::State::kInProgress:
        return;
      case drivefs::mojom::ItemEvent::State::kCompleted:
        // The file has fully synced.
        OnEndCopy();
        return;
      case drivefs::mojom::ItemEvent::State::kFailed:
        LOG(ERROR) << "Drive sync error: failed";
        OnEndCopy(MigrationUploadError::kSyncFailed);
        return;
      case drivefs::mojom::ItemEvent::State::kCancelledAndDeleted:
        NOTREACHED();
      case drivefs::mojom::ItemEvent::State::kCancelledAndTrashed:
        LOG(ERROR) << "Drive sync error: cancelled and trashed";
        OnEndCopy(MigrationUploadError::kSyncFailed);
        return;
    }
  }
}

void DriveSkyvaultUploader::OnError(const drivefs::mojom::DriveError& error) {
  if (base::FilePath(error.path) != observed_relative_drive_path_) {
    return;
  }
  OnEndCopy(MigrationUploadError::kCloudQuotaFull);
}

void DriveSkyvaultUploader::OnDriveConnectionStatusChanged(
    drive::util::ConnectionStatus status) {
  if (waiting_for_connection_) {
    if (status == drive::util::ConnectionStatus::kConnected) {
      LOG(ERROR) << "Reconnected to Drive";
      waiting_for_connection_ = false;
      CHECK(connection_wait_start_time_.has_value());
      SkyVaultMigrationReconnectionDurationHistogram(
          MigrationDestination::kGoogleDrive,
          base::Time::Now() - connection_wait_start_time_.value());
      connection_wait_start_time_.reset();
      reconnection_timer_.Stop();
      drive_observation_.Reset();
      Run();
    }
    return;
  }
  if (status == drive::util::ConnectionStatus::kNoService) {
    LOG(ERROR) << "Drive service became unavailable during upload";
    OnEndCopy(MigrationUploadError::kServiceUnavailable);
    return;
  }
  if (status != drive::util::ConnectionStatus::kConnected) {
    LOG(ERROR) << "Lost connection to Drive during upload";
    OnEndCopy(MigrationUploadError::kNetworkError);
  }
}

void DriveSkyvaultUploader::OnDriveIntegrationServiceDestroyed() {
  drive_observation_.Reset();
}

void DriveSkyvaultUploader::OnReconnectionTimeout() {
  if (!waiting_for_connection_) {
    LOG(ERROR) << "Reconnection timer fired, but currently not waiting for "
                  "connection; ignoring";
    return;
  }
  LOG(ERROR)
      << "Reconnection not established within the timeout, failing the upload";
  OnEndCopy(MigrationUploadError::kReconnectTimeout);
}

}  // namespace policy::local_user_files
