// Copyright 2019 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/extensions/api/printing/printing_api_handler.h"

#include <utility>

#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner.h"
#include "base/types/to_address.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/ash/printing/cups_print_job.h"
#include "chrome/browser/ash/printing/local_printer.h"
#include "chrome/browser/ash/printing/print_management/printing_manager.h"
#include "chrome/browser/ash/printing/print_management/printing_manager_factory.h"
#include "chrome/browser/chromeos/printing/cups_wrapper.h"
#include "chrome/browser/chromeos/printing/printer_error_codes.h"
#include "chrome/browser/extensions/api/printing/print_job_submitter.h"
#include "chrome/browser/extensions/api/printing/printing_api_utils.h"
#include "chrome/browser/printing/pdf_blob_data_flattener.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/browser/printing/print_job_controller.h"
#include "chrome/browser/printing/print_preview_sticky_settings.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/printing/common/cloud_print_cdd_conversion.h"
#include "components/session_manager/core/session.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_registry.h"
#include "printing/print_settings.h"
#include "printing/printed_document.h"

namespace extensions {

namespace {

// Maximum value for finished jobs we'd like to store to avoid missusage of
// the API and potential OOMs.
static constexpr size_t kMaximumFinishedJobs = 10000u;
// The number of jobs to remove from the cache once the cache is full. We
// remove these in batches so we don't have to immediately clear the cache
// again.
static constexpr size_t kNumOfJobsToRemove = 51u;

constexpr char kNoActivePrintJobWithIdError[] =
    "No active print job with given ID";
constexpr char kNoPrintJobWithIdError[] = "No print job with given ID";

bool PrintJobReachedTerminalState(const api::printing::JobStatus& status) {
  return status == api::printing::JobStatus::kPrinted ||
         status == api::printing::JobStatus::kFailed ||
         status == api::printing::JobStatus::kCanceled;
}

}  // namespace

// static
std::unique_ptr<PrintingAPIHandler> PrintingAPIHandler::CreateForTesting(
    content::BrowserContext* browser_context,
    EventRouter* event_router,
    ExtensionRegistry* extension_registry,
    std::unique_ptr<printing::PrintJobController> print_job_controller,
    std::unique_ptr<chromeos::CupsWrapper> cups_wrapper,
    ash::LocalPrinter* local_printer) {
  return std::make_unique<PrintingAPIHandler>(
      browser_context, event_router, extension_registry,
      std::move(print_job_controller), std::move(cups_wrapper), local_printer);
}

PrintingAPIHandler::PrintingAPIHandler(content::BrowserContext* browser_context)
    : PrintingAPIHandler(browser_context,
                         EventRouter::Get(browser_context),
                         ExtensionRegistry::Get(browser_context),
                         std::make_unique<printing::PrintJobController>(),
                         chromeos::CupsWrapper::Create(),
                         ash::LocalPrinter::Get()) {
  CHECK(user_manager::UserManager::IsInitialized());
  CHECK(user_manager::UserManager::Get()->IsUserLoggedIn());
  CHECK(session_manager::SessionManager::Get()->GetPrimarySession());
  // TODO(crbug.com/354842935): Replace by ash::AnnotatedAccountId.
  // TODO(crbug.com/479647640): Check if we should use current user than
  // primary user.
  AccountId account_id =
      session_manager::SessionManager::Get()->GetPrimarySession()->account_id();
  auto* primary_context =
      ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
          account_id);
  ash::CupsPrintJobManager* print_job_manager =
      ash::CupsPrintJobManagerFactory::GetForBrowserContext(primary_context);
  CHECK(print_job_manager);
  observation_.Observe(print_job_manager);
}

PrintingAPIHandler::PrintingAPIHandler(
    content::BrowserContext* browser_context,
    EventRouter* event_router,
    ExtensionRegistry* extension_registry,
    std::unique_ptr<printing::PrintJobController> print_job_controller,
    std::unique_ptr<chromeos::CupsWrapper> cups_wrapper,
    ash::LocalPrinter* local_printer)
    : browser_context_(browser_context),
      event_router_(event_router),
      extension_registry_(extension_registry),
      print_job_controller_(std::move(print_job_controller)),
      cups_wrapper_(std::move(cups_wrapper)),
      pdf_blob_data_flattener_(std::make_unique<printing::PdfBlobDataFlattener>(
          Profile::FromBrowserContext(browser_context))),
      local_printer_(CHECK_DEREF(local_printer)) {}

PrintingAPIHandler::~PrintingAPIHandler() = default;

// static
std::string PrintingAPIHandler::CreateUniqueId(const std::string& printer_id,
                                               int job_id) {
  return base::StringPrintf("%s%d", printer_id.c_str(), job_id);
}

// static
BrowserContextKeyedAPIFactory<PrintingAPIHandler>*
PrintingAPIHandler::GetFactoryInstance() {
  static base::NoDestructor<BrowserContextKeyedAPIFactory<PrintingAPIHandler>>
      instance;
  return instance.get();
}

// static
PrintingAPIHandler* PrintingAPIHandler::Get(
    content::BrowserContext* browser_context) {
  return BrowserContextKeyedAPIFactory<PrintingAPIHandler>::Get(
      browser_context);
}

// static
void PrintingAPIHandler::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterListPref(prefs::kPrintingAPIExtensionsAllowlist);
}

void PrintingAPIHandler::SubmitJob(
    gfx::NativeWindow native_window,
    scoped_refptr<const extensions::Extension> extension,
    std::optional<api::printing::SubmitJob::Params> params,
    SubmitJobCallback callback) {
  DCHECK(params);
  // PrintingAPIHandler must outlive PrintJobSubmitter. Even if the WeakPtr
  // expires, PrintJobSubmitter will continue to access PrintingAPIHandler
  // member variables.

  std::string extension_id = extension->id();
  PrintJobSubmitter::Run(std::make_unique<PrintJobSubmitter>(
      native_window, browser_context_, print_job_controller_.get(),
      pdf_blob_data_flattener_.get(), std::move(extension),
      std::move(params->request), &*local_printer_,
      base::BindOnce(&PrintingAPIHandler::OnPrintJobSubmitted,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                     std::move(extension_id))));
}

void PrintingAPIHandler::OnPrintJobSubmitted(
    SubmitJobCallback callback,
    const std::string& extension_id,
    PrintJobSubmitter::PrintJobCreationResult result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!result.has_value()) {
    std::optional<std::string> error = std::move(result).error();
    std::optional<api::printing::SubmitJobStatus> status;
    if (!error) {
      status = api::printing::SubmitJobStatus::kUserRejected;
    }
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), status, std::nullopt,
                                  std::move(error)));
    return;
  }

  printing::PrintJobCreatedInfo info = std::move(result).value();
  std::string printer_id =
      base::UTF16ToUTF8(info.document->settings().device_name());
  DCHECK(!printer_id.empty());

  std::string cups_id = CreateUniqueId(printer_id, info.job_id);

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), api::printing::SubmitJobStatus::kOk,
                     cups_id, std::nullopt));

  DCHECK(!in_progress_print_jobs_.contains(cups_id));
  constexpr api::printing::JobStatus job_status =
      api::printing::JobStatus::kPending;
  in_progress_print_jobs_[cups_id] =
      PrintJobInfo{printer_id, info.job_id, extension_id, job_status};

  if (!extension_registry_->enabled_extensions().Contains(extension_id)) {
    return;
  }

  auto event = std::make_unique<Event>(
      events::PRINTING_ON_JOB_STATUS_CHANGED,
      api::printing::OnJobStatusChanged::kEventName,
      api::printing::OnJobStatusChanged::Create(cups_id, job_status));
  event_router_->DispatchEventToExtension(extension_id, std::move(event));
}

std::optional<std::string> PrintingAPIHandler::CancelJob(
    const std::string& extension_id,
    const std::string& job_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  auto it = in_progress_print_jobs_.find(job_id);

  // If there was no print job with specified id sent by the extension return
  // an error.
  if (it == in_progress_print_jobs_.end() ||
      it->second.extension_id != extension_id) {
    return kNoActivePrintJobWithIdError;
  }

  CHECK(user_manager::UserManager::IsInitialized());
  CHECK(user_manager::UserManager::Get()->IsUserLoggedIn());
  CHECK(session_manager::SessionManager::Get()->GetPrimarySession());
  // TODO(crbug.com/354842935): Replace by ash::AnnotatedAccountId.
  // TODO(crbug.com/479647640): Check if we should use current user than
  // primary user.
  AccountId account_id =
      session_manager::SessionManager::Get()->GetPrimarySession()->account_id();
  auto* primary_context =
      ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
          account_id);
  ash::printing::print_management::PrintingManagerFactory::GetForProfile(
      Profile::FromBrowserContext(primary_context))
      ->CancelPrintJob(CreateUniqueId(it->second.printer_id, it->second.job_id),
                       base::DoNothing());

  return std::nullopt;
}

void PrintingAPIHandler::GetPrinters(GetPrintersCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(user_manager::UserManager::IsInitialized());
  CHECK(user_manager::UserManager::Get()->IsUserLoggedIn());

  local_printer_->GetPrinters(
      // TODO(crbug.com/354842935): Replace by ash::AnnotatedAccountId.
      user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId(),
      base::BindOnce(&PrintingAPIHandler::OnPrintersRetrieved,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void PrintingAPIHandler::OnPrintersRetrieved(
    GetPrintersCallback callback,
    std::vector<chromeos::Printer> printers) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  PrefService* prefs =
      Profile::FromBrowserContext(browser_context_)->GetPrefs();

  std::optional<DefaultPrinterRules> default_printer_rules =
      GetDefaultPrinterRules(prefs->GetString(
          prefs::kPrintPreviewDefaultDestinationSelectionRules));

  auto* sticky_settings = printing::PrintPreviewStickySettings::GetInstance();
  sticky_settings->RestoreFromPrefs(prefs);
  base::flat_map<std::string, int> recently_used_ranks =
      sticky_settings->GetPrinterRecentlyUsedRanks();

  std::vector<api::printing::Printer> printers_api;
  printers_api.reserve(printers.size());
  for (const chromeos::Printer& printer : printers) {
    printers_api.push_back(
        PrinterToIdl(printer, default_printer_rules, recently_used_ranks));
  }
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::move(printers_api)));
}

void PrintingAPIHandler::GetPrinterInfo(const std::string& printer_id,
                                        GetPrinterInfoCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(user_manager::UserManager::IsInitialized());
  CHECK(user_manager::UserManager::Get()->IsUserLoggedIn());

  local_printer_->GetCapability(
      // TODO(crbug.com/354842935): Replace by ash::AnnotatedAccountId.
      user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId(),
      printer_id,
      base::BindOnce(&PrintingAPIHandler::OnPrinterCapabilitiesRetrieved,
                     weak_ptr_factory_.GetWeakPtr(), printer_id,
                     std::move(callback)));
}

void PrintingAPIHandler::OnPrinterCapabilitiesRetrieved(
    const std::string& printer_id,
    GetPrinterInfoCallback callback,
    base::optional_ref<const chromeos::Printer> /*printer*/,
    const std::optional<printing::PrinterSemanticCapsAndDefaults>& caps) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!caps.has_value()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), /*capabilities=*/std::nullopt,
                       /*status=*/api::printing::PrinterStatus::kUnreachable,
                       /*error=*/std::nullopt));
    return;
  }

  cups_wrapper_->QueryCupsPrinterStatus(
      printer_id,
      base::BindOnce(
          &PrintingAPIHandler::OnPrinterStatusRetrieved,
          weak_ptr_factory_.GetWeakPtr(), std::move(callback),
          cloud_print::PrinterSemanticCapsAndDefaultsToCdd(caps.value())));
}

void PrintingAPIHandler::OnPrinterStatusRetrieved(
    GetPrinterInfoCallback callback,
    base::Value capabilities,
    std::unique_ptr<::printing::PrinterStatus> printer_status) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!printer_status) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), std::move(capabilities),
                                  api::printing::PrinterStatus::kUnreachable,
                                  /*error=*/std::nullopt));
    return;
  }
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          std::move(callback), std::move(capabilities),
          PrinterStatusToIdl(chromeos::PrinterErrorCodeFromPrinterStatusReasons(
              *printer_status)),
          /*error=*/std::nullopt));
}

void PrintingAPIHandler::MaybeEvictFinishedPrintJobs() {
  if (finished_print_jobs_.size() <= kMaximumFinishedJobs) {
    return;
  }

  DCHECK_EQ(kMaximumFinishedJobs + 1u, finished_jobs_order_.size());
  DCHECK_EQ(finished_jobs_order_.size(), finished_print_jobs_.size());

  size_t removed_jobs = 0;
  while (finished_jobs_order_.size() > 0 &&
         removed_jobs++ < kNumOfJobsToRemove) {
    finished_print_jobs_.erase(finished_jobs_order_.front());
    finished_jobs_order_.pop_front();
  }
}

base::expected<api::printing::JobStatus, std::string>
PrintingAPIHandler::GetJobStatus(const std::string& extension_id,
                                 const std::string& job_id) {
  // First, search in inprogress jobs.
  auto in_progress_job_it = in_progress_print_jobs_.find(job_id);
  if (in_progress_job_it != in_progress_print_jobs_.end() &&
      in_progress_job_it->second.extension_id == extension_id) {
    return base::ok(in_progress_job_it->second.status);
  }
  // If not found, try finished jobs. See the declaration of these containers to
  // get more information on their usages.
  auto finished_job_it = finished_print_jobs_.find(job_id);
  if (finished_job_it != finished_print_jobs_.end() &&
      /*extension_id=*/finished_job_it->second.first == extension_id) {
    return base::ok(/*status=*/finished_job_it->second.second);
  }
  return base::unexpected(kNoPrintJobWithIdError);
}

void PrintingAPIHandler::OnPrintJobStarted(
    base::WeakPtr<ash::CupsPrintJob> job) {
  if (job) {
    UpdateJobStatus(job->printer().id(), job->job_id(),
                    api::printing::JobStatus::kInProgress);
  }
}

void PrintingAPIHandler::OnPrintJobDone(base::WeakPtr<ash::CupsPrintJob> job) {
  if (job) {
    UpdateJobStatus(job->printer().id(), job->job_id(),
                    api::printing::JobStatus::kPrinted);
  }
}

void PrintingAPIHandler::OnPrintJobError(base::WeakPtr<ash::CupsPrintJob> job) {
  if (job) {
    UpdateJobStatus(job->printer().id(), job->job_id(),
                    api::printing::JobStatus::kFailed);
  }
}

void PrintingAPIHandler::OnPrintJobCancelled(
    base::WeakPtr<ash::CupsPrintJob> job) {
  if (job) {
    UpdateJobStatus(job->printer().id(), job->job_id(),
                    api::printing::JobStatus::kCanceled);
  }
}

void PrintingAPIHandler::UpdateJobStatus(const std::string& printer_id,
                                         int job_id,
                                         api::printing::JobStatus job_status) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::string cups_id = CreateUniqueId(printer_id, job_id);
  auto it = in_progress_print_jobs_.find(cups_id);
  if (it == in_progress_print_jobs_.end()) {
    return;
  }
  it->second.status = job_status;

  const std::string& extension_id = it->second.extension_id;
  if (extension_registry_->enabled_extensions().Contains(extension_id)) {
    auto event = std::make_unique<Event>(
        events::PRINTING_ON_JOB_STATUS_CHANGED,
        api::printing::OnJobStatusChanged::kEventName,
        api::printing::OnJobStatusChanged::Create(cups_id, job_status));
    event_router_->DispatchEventToExtension(extension_id, std::move(event));
  }

  // Treat only those jobs finished, which are completed, failed, or cancelled.
  if (PrintJobReachedTerminalState(it->second.status)) {
    DCHECK(!finished_print_jobs_.contains(cups_id));
    finished_print_jobs_[cups_id] =
        std::make_pair(it->second.extension_id, it->second.status);
    finished_jobs_order_.push_back(cups_id);
    in_progress_print_jobs_.erase(it);
    MaybeEvictFinishedPrintJobs();
  }
}

template <>
std::unique_ptr<KeyedService>
BrowserContextKeyedAPIFactory<PrintingAPIHandler>::
    BuildServiceInstanceForBrowserContext(
        content::BrowserContext* context) const {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  Profile* profile = Profile::FromBrowserContext(context);
  // We do not want an instance of PrintingAPIHandler on the lock screen.
  // This will lead to multiple printing notifications.
  if (!profile->IsRegularProfile()) {
    return nullptr;
  }
  return std::make_unique<PrintingAPIHandler>(context);
}

}  // namespace extensions
