// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/download/model/download_file_service.h"

#import <Foundation/Foundation.h>

#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/thread_pool.h"
#import "base/uuid.h"
#import "ios/chrome/browser/download/model/download_directory_util.h"
#import "ios/chrome/browser/download/model/download_record_service.h"

DownloadFileService::DownloadFileService(
    DownloadRecordService* download_record_service)
    : file_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN})),
      download_record_service_(download_record_service) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_);
}

DownloadFileService::~DownloadFileService() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_);
}

void DownloadFileService::MoveDownloadFile(
    const std::string& download_id,
    const base::FilePath& source_path,
    const base::FilePath& destination_path,
    MoveCompleteCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_);

  if (download_id.empty() || source_path.empty() || destination_path.empty()) {
    if (callback) {
      std::move(callback).Run(false, download_id, source_path,
                              destination_path);
    }
    return;
  }

  file_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&DownloadFileService::DoMoveFileOnBackgroundThread,
                     file_task_runner_, source_path, destination_path),
      base::BindOnce(&DownloadFileService::OnFileMoveComplete,
                     weak_ptr_factory_.GetWeakPtr(), download_id, source_path,
                     std::move(callback)));
}

void DownloadFileService::ResolveAvailableFilePath(
    const base::FilePath& target_directory,
    const base::FilePath& suggested_filename,
    base::OnceCallback<void(base::FilePath)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_);

  file_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&DownloadFileService::FindAvailableDownloadFilePath,
                     file_task_runner_, target_directory, suggested_filename),
      std::move(callback));
}

void DownloadFileService::CheckFileExists(
    const base::FilePath& file_path,
    base::OnceCallback<void(bool)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_);

  file_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(base::PathExists, file_path),
      std::move(callback));
}

void DownloadFileService::OnFileMoveComplete(const std::string& download_id,
                                             const base::FilePath& source_path,
                                             MoveCompleteCallback callback,
                                             base::FilePath final_path) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_);

  const bool move_success = !final_path.empty();
  if (move_success && download_record_service_) {
    // Convert absolute path to relative path for storage.
    base::FilePath relative_path = ConvertToRelativeDownloadPath(final_path);
    download_record_service_->UpdateDownloadFilePathAsync(download_id,
                                                          relative_path);
  }

  if (callback) {
    std::move(callback).Run(move_success, download_id, source_path, final_path);
  }
}

base::FilePath DownloadFileService::DoMoveFileOnBackgroundThread(
    scoped_refptr<base::SequencedTaskRunner> file_task_runner,
    const base::FilePath& source_path,
    const base::FilePath& destination_path) {
  DCHECK(file_task_runner->RunsTasksInCurrentSequence());

  // Check if source file exists.
  if (!base::PathExists(source_path)) {
    return base::FilePath();
  }

  // Create destination directory if it doesn't exist.
  base::FilePath destination_dir = destination_path.DirName();
  if (!base::CreateDirectory(destination_dir)) {
    return base::FilePath();
  }

  // If a file already exists at the requested destination (e.g. another
  // download with the same suggested name completed first), pick the next
  // available name so the existing file is not overwritten.
  base::FilePath final_path = destination_path;
  if (base::PathExists(final_path)) {
    final_path = FindAvailableDownloadFilePath(
        file_task_runner, destination_dir, destination_path.BaseName());
  }

  // Move the file.
  if (!base::Move(source_path, final_path)) {
    return base::FilePath();
  }

  return final_path;
}

base::FilePath DownloadFileService::FindAvailableDownloadFilePath(
    scoped_refptr<base::SequencedTaskRunner> file_task_runner,
    const base::FilePath& download_dir,
    const base::FilePath& file_name) {
  DCHECK(file_task_runner->RunsTasksInCurrentSequence());

  base::FilePath actual_file_name = file_name;
  // If the suggested `file_name` is empty or '.' or '..' then it is replaced
  // with a randomly generated UUID.
  if (actual_file_name.empty() ||
      actual_file_name.value() == base::FilePath::kCurrentDirectory ||
      actual_file_name.value() == base::FilePath::kParentDirectory) {
    actual_file_name =
        base::FilePath(base::Uuid::GenerateRandomV4().AsLowercaseString());
  }

  base::FilePath candidate_file_name = actual_file_name;
  int number_of_attempts = 0;
  while (base::PathExists(download_dir.Append(candidate_file_name))) {
    number_of_attempts++;
    candidate_file_name = actual_file_name.InsertBeforeExtension(
        " (" + base::NumberToString(number_of_attempts) + ")");
  }
  return download_dir.Append(candidate_file_name);
}
