// Copyright 2018 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_manager_tab_helper.h"

#import <optional>

#import "base/check_op.h"
#import "base/feature_list.h"
#import "base/files/file_path.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/strings/sys_string_conversions.h"
#import "components/enterprise/connectors/core/cloud_content_scanning/files_request_handler_base.h"
#import "components/policy/core/common/policy_pref_names.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/download/model/auto_deletion/auto_deletion_service.h"
#import "ios/chrome/browser/download/model/browser_download_service.h"
#import "ios/chrome/browser/download/model/download_directory_util.h"
#import "ios/chrome/browser/download/model/download_file_service.h"
#import "ios/chrome/browser/download/model/download_file_service_factory.h"
#import "ios/chrome/browser/download/model/download_manager_tab_helper_delegate.h"
#import "ios/chrome/browser/drive/model/drive_availability.h"
#import "ios/chrome/browser/drive/model/drive_policy.h"
#import "ios/chrome/browser/drive/model/drive_service_factory.h"
#import "ios/chrome/browser/drive/model/drive_tab_helper.h"
#import "ios/chrome/browser/drive/model/upload_task.h"
#import "ios/chrome/browser/enterprise/cloud_content_scanning/model/cloud_content_scanning_helper.h"
#import "ios/chrome/browser/enterprise/connectors/analysis/content_analysis_info.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/utils/mime_type_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/browser_util.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/download/download_task.h"
#import "ui/base/l10n/l10n_util_mac.h"

DownloadManagerTabHelper::DownloadManagerTabHelper(web::WebState* web_state)
    : web_state_(web_state) {
  DCHECK(web_state_);
  web_state_->AddObserver(this);
}

DownloadManagerTabHelper::~DownloadManagerTabHelper() {
  if (web_state_) {
    web_state_->RemoveObserver(this);
    web_state_ = nullptr;
  }

  CleanupCurrentDownload();
}

#pragma mark - Public methods

void DownloadManagerTabHelper::SetCurrentDownload(
    std::unique_ptr<web::DownloadTask> task) {
  // If downloads are persistent, they cannot be lost once completed.
  if (!task_ || (task_->GetState() == web::DownloadTask::State::kComplete &&
                 !WillDownloadTaskBeSavedToDrive())) {
    // The task is the first download for this web state.
    DidCreateDownload(std::move(task));
    return;
  }

  // If there is no new task and an existing task is present, remove the
  // observer and reset the task.
  if (!task) {
    CleanupCurrentDownload();
    return;
  }

  // Capture a raw pointer to `task` before moving it into `callback`.
  web::DownloadTask* task_ptr = task.get();
  auto callback =
      base::BindOnce(&DownloadManagerTabHelper::OnDownloadPolicyDecision,
                     weak_ptr_factory_.GetWeakPtr(), std::move(task));

  [delegate_
      downloadManagerTabHelper:this
       decidePolicyForDownload:task_ptr
             completionHandler:base::CallbackToBlock(std::move(callback))];
}

const base::FilePath& DownloadManagerTabHelper::GetDownloadTaskFinalFilePath()
    const {
  return task_final_file_path_;
}

void DownloadManagerTabHelper::SetDelegate(
    id<DownloadManagerTabHelperDelegate> delegate) {
  if (delegate == delegate_) {
    return;
  }

  if (delegate_ && task_ && delegate_started_) {
    [delegate_ downloadManagerTabHelper:this
                        didHideDownload:task_.get()
                               animated:NO];
  }

  delegate_started_ = false;
  delegate_ = delegate;
}

void DownloadManagerTabHelper::SetSnackbarHandler(
    id<SnackbarCommands> snackbar_handler) {
  snackbar_handler_ = snackbar_handler;
}

void DownloadManagerTabHelper::StartDownload(web::DownloadTask* task) {
  DCHECK_EQ(task, task_.get());
  [delegate_ downloadManagerTabHelper:this wantsToStartDownload:task_.get()];
}

web::DownloadTask* DownloadManagerTabHelper::GetActiveDownloadTask() {
  return task_.get();
}

void DownloadManagerTabHelper::CleanupCurrentDownload() {
  if (task_) {
    if (delegate_ && delegate_started_) {
      delegate_started_ = false;
      [delegate_ downloadManagerTabHelper:this didCleanupDownload:task_.get()];
    }
    task_->RemoveObserver(this);
    task_.reset();
    task_final_file_path_.clear();
  }
  files_request_handler_.reset();
  content_analysis_info_.reset();
  has_processed_complete_task_ = false;
}

void DownloadManagerTabHelper::AdaptToFullscreen(bool adapt_to_fullscreen) {
  if (delegate_ && delegate_started_) {
    [delegate_ downloadManagerTabHelper:this
                      adaptToFullscreen:adapt_to_fullscreen];
  }
}

bool DownloadManagerTabHelper::IsScannerProcessing() const {
  if (is_processing_for_testing_) {
    return true;
  }
  return files_request_handler_ != nullptr;
}

void DownloadManagerTabHelper::SetIsScannerProcessingForTesting(  // IN-TEST
    bool processing) {
  is_processing_for_testing_ = processing;
}

bool DownloadManagerTabHelper::WillDownloadTaskBeSavedToDrive() const {
  DriveTabHelper* drive_tab_helper =
      DriveTabHelper::FromWebState(task_->GetWebState());
  UploadTask* upload_task =
      drive_tab_helper->GetUploadTaskForDownload(task_.get());
  return upload_task && !upload_task->IsDone();
}

#pragma mark - web::WebStateObserver

void DownloadManagerTabHelper::WasShown(web::WebState* web_state) {
  if (task_ && delegate_ && !delegate_started_) {
    if (BrowserDownloadService::ShouldRestrictAllDownloads(web_state_)) {
      SetCurrentDownload(nullptr);
      return;
    }

    delegate_started_ = true;
    [delegate_ downloadManagerTabHelper:this
                        didShowDownload:task_.get()
                               animated:NO];
  }
}

void DownloadManagerTabHelper::WasHidden(web::WebState* web_state) {
  if (task_ && delegate_ && delegate_started_) {
    delegate_started_ = false;
    [delegate_ downloadManagerTabHelper:this
                        didHideDownload:task_.get()
                               animated:NO];
  }
}

void DownloadManagerTabHelper::WebStateDestroyed(web::WebState* web_state) {
  DCHECK_EQ(web_state_, web_state);
  web_state_->RemoveObserver(this);
  web_state_ = nullptr;
  CleanupCurrentDownload();
}

#pragma mark - web::DownloadTaskObserver

void DownloadManagerTabHelper::OnDownloadUpdated(web::DownloadTask* task) {
  DCHECK_EQ(task, task_.get());
  switch (task->GetState()) {
    case web::DownloadTask::State::kCancelled:
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(&DownloadManagerTabHelper::CleanupCurrentDownload,
                         weak_ptr_factory_.GetWeakPtr()));
      break;
    case web::DownloadTask::State::kInProgress:
      break;
    case web::DownloadTask::State::kComplete:
      DownloadManagerTabHelper::ProcessCompleteDownloadTask();
      break;
    case web::DownloadTask::State::kFailed:
    case web::DownloadTask::State::kFailedNotResumable:
      break;
    case web::DownloadTask::State::kNotStarted:
      // OnDownloadUpdated cannot be called with this state.
      NOTREACHED();
  }
}

#pragma mark - Private

void DownloadManagerTabHelper::DidCreateDownload(
    std::unique_ptr<web::DownloadTask> task) {
  CleanupCurrentDownload();
  task_ = std::move(task);
  task_->AddObserver(this);
  if (web_state_->IsVisible() && delegate_) {
    delegate_started_ = true;
    [delegate_ downloadManagerTabHelper:this
                      didCreateDownload:task_.get()
                      webStateIsVisible:true];
  }

  MaybeEnrollFileForAutoDeletion(task_.get());
}

void DownloadManagerTabHelper::OnDownloadPolicyDecision(
    std::unique_ptr<web::DownloadTask> task,
    NewDownloadPolicy policy) {
  if (policy == kNewDownloadPolicyReplace) {
    DidCreateDownload(std::move(task));
  }
}

void DownloadManagerTabHelper::ShowRestrictDownloadSnackbar() {
  if (!snackbar_handler_) {
    return;
  }

  [snackbar_handler_
      showSnackbarWithMessage:l10n_util::GetNSString(
                                  IDS_IOS_DOWNLOAD_RESTRICTION_SNACKBAR_TEXT)
                   buttonText:
                       l10n_util::GetNSString(
                           IDS_IOS_DOWNLOAD_RESTRICTION_SNACKBAR_BUTTON_TEXT)
                messageAction:nil
             completionAction:nil];
}

void DownloadManagerTabHelper::UseAvailableUserDocumentsPath(
    base::FilePath user_documents_path) {
  if (!task_) {
    return;
  }

  task_final_file_path_ = std::move(user_documents_path);
  base::FilePath task_path = task_->GetResponsePath();

  GetDownloadFileService()->CheckFileExists(
      task_path,
      base::BindOnce(&DownloadManagerTabHelper::MoveToUserDocumentsIfFileExists,
                     weak_ptr_factory_.GetWeakPtr(), task_path));
}

void DownloadManagerTabHelper::MoveToUserDocumentsIfFileExists(
    base::FilePath task_path,
    bool file_exists) {
  if (!file_exists || !task_) {
    return;
  }

  std::string download_id = base::SysNSStringToUTF8(task_->GetIdentifier());

  GetDownloadFileService()->MoveDownloadFile(
      download_id, task_path, GetDownloadTaskFinalFilePath(),
      base::BindOnce(&DownloadManagerTabHelper::MoveComplete,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DownloadManagerTabHelper::MoveComplete(bool move_completed,
                                            const std::string& download_id,
                                            const base::FilePath& source_path,
                                            const base::FilePath& final_path) {
  DCHECK(move_completed);
  if (move_completed) {
    task_final_file_path_ = final_path;
  }
  MaybeSetDownloadPathForAutoDeletion();
  [delegate_ downloadManagerTabHelperDidChangeState:this];
}

void DownloadManagerTabHelper::MaybeEnrollFileForAutoDeletion(
    web::DownloadTask* task) {
  PrefService* localState = GetApplicationContext()->GetLocalState();
  BOOL isAutoDeletionEnabled =
      localState->GetBoolean(prefs::kDownloadAutoDeletionEnabled);
  if (!IsDownloadAutoDeletionFeatureEnabled() || !isAutoDeletionEnabled) {
    return;
  }

  auto_deletion::AutoDeletionService* service =
      GetApplicationContext()->GetAutoDeletionService();
  service->SetDownloadTask(task);
}

void DownloadManagerTabHelper::MaybeSetDownloadPathForAutoDeletion() {
  PrefService* localState = GetApplicationContext()->GetLocalState();
  BOOL isAutoDeletionEnabled =
      localState->GetBoolean(prefs::kDownloadAutoDeletionEnabled);
  if (!IsDownloadAutoDeletionFeatureEnabled() || !isAutoDeletionEnabled) {
    return;
  }

  auto_deletion::AutoDeletionService* service =
      GetApplicationContext()->GetAutoDeletionService();
  service->SetDownloadPath(GetDownloadTaskFinalFilePath());
}

DownloadFileService* DownloadManagerTabHelper::GetDownloadFileService() {
  CHECK(web_state_);

  ProfileIOS* profile =
      ProfileIOS::FromBrowserState(web_state_->GetBrowserState());
  DownloadFileService* download_file_service =
      DownloadFileServiceFactory::GetForProfile(profile);

  CHECK(download_file_service);
  return download_file_service;
}

void DownloadManagerTabHelper::MaybeMoveDownloadToDownloadsDirectory(
    base::WeakPtr<web::DownloadTask> task,
    bool shouldProceed) {
  // Ignore the result if it does not correspond to the current download.
  if (!task || task.get() != task_.get()) {
    return;
  }

  if (!shouldProceed) {
    CleanupCurrentDownload();
    return;
  }

  // This will only report when scan result is WARNING and bypassed.
  files_request_handler_->ReportWarningBypass(
      /* user_justification */ std::nullopt);

  [delegate_ downloadManagerTabHelperDidChangeState:this];
  base::FilePath user_download_path;
  GetDownloadsDirectory(&user_download_path);
  base::FilePath base_file_name = task_->GenerateFileName();

  GetDownloadFileService()->ResolveAvailableFilePath(
      user_download_path, base_file_name,
      base::BindOnce(&DownloadManagerTabHelper::UseAvailableUserDocumentsPath,
                     weak_ptr_factory_.GetWeakPtr()));

  // Ensure the handler and content_analysis_info_ are destroyed as soon as they
  // are no longer necessary.
  files_request_handler_.reset();
  content_analysis_info_.reset();
}

void DownloadManagerTabHelper::ProcessCompleteDownloadTask() {
  if (WillDownloadTaskBeSavedToDrive()) {
    return;
  }

  if (has_processed_complete_task_) {
    return;
  }
  has_processed_complete_task_ = true;

  enterprise_connectors::FileDownloadScanningResources resources =
      enterprise_connectors::PrepareCloudContentScanning(
          web_state_, task_->GetRedirectedUrl(), task_->GetResponsePath(),
          enterprise_connectors::TriggerType::kSavePrompt,
          base::BindOnce(
              &DownloadManagerTabHelper::MaybeMoveDownloadToDownloadsDirectory,
              weak_ptr_factory_.GetWeakPtr(), task_->GetWeakPtr()));
  files_request_handler_ = std::move(resources.files_request_handler);
  content_analysis_info_ = std::move(resources.content_analysis_info);

  // Upload the file for content scanning.
  files_request_handler_->UploadData();
}
