// Copyright 2017 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/printing/printer_configurer.h"

#include <map>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "ash/constants/webui_url_constants.h"
#include "base/check.h"
#include "base/check_deref.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_view_util.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice_client.h"
#include "chromeos/ash/components/dbus/printscanmgr/printscanmgr_client.h"
#include "chromeos/dbus/common/dbus_library_error.h"
#include "chromeos/printing/ppd_line_reader.h"
#include "chromeos/printing/ppd_provider.h"
#include "chromeos/printing/printer_configuration.h"
#include "components/application_locale_storage/application_locale_storage.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/obsolete/md5.h"
#include "third_party/cros_system_api/dbus/debugd/dbus-constants.h"

namespace ash {

namespace printing {
// Not placed in namespace {} so it can be friended from //crypto.
crypto::obsolete::Md5 MakeMd5HasherForPrinterConfigurer() {
  return {};
}
}  // namespace printing

namespace {

using ::chromeos::PpdProvider;
using ::chromeos::Printer;

const char kEbuildWithHplipPlugins[] = "hplip-plugin";

PrinterSetupResult PrinterSetupResultFromAddPrinterResult(
    const Printer& printer,
    printscanmgr::AddPrinterResult result) {
  const std::string prefix =
      printer.id() + ":" + printer.make_and_model() + " setup result: ";
  switch (result) {
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_SUCCESS:
      PRINTER_LOG(EVENT) << prefix << "Printer setup successful";
      return PrinterSetupResult::kSuccess;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_CUPS_INVALID_PPD:
      PRINTER_LOG(EVENT) << prefix << "PPD Invalid";
      return PrinterSetupResult::kInvalidPpd;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_LPADMIN_FAILURE:
      PRINTER_LOG(ERROR) << prefix << "lpadmin-manual failed";
      return PrinterSetupResult::kFatalError;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_AUTOCONF_FAILURE:
      PRINTER_LOG(ERROR) << prefix << "lpadmin-autoconf failed";
      return PrinterSetupResult::kFatalError;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_CUPS_BAD_URI:
      PRINTER_LOG(EVENT) << prefix << "Bad URI";
      return PrinterSetupResult::kBadUri;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_CUPS_IO_ERROR:
      PRINTER_LOG(ERROR) << prefix << "I/O error";
      return PrinterSetupResult::kIoError;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_MEMORY_ALLOC_ERROR:
      PRINTER_LOG(EVENT) << prefix << "Memory allocation error";
      return PrinterSetupResult::kMemoryAllocationError;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_PRINTER_UNREACHABLE:
      PRINTER_LOG(EVENT) << prefix << "Printer is unreachable";
      return PrinterSetupResult::kPrinterUnreachable;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_PRINTER_WRONG_RESPONSE:
      PRINTER_LOG(EVENT) << prefix << "Unexpected response from printer";
      return PrinterSetupResult::kPrinterSentWrongResponse;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_PRINTER_NOT_AUTOCONF:
      PRINTER_LOG(EVENT) << prefix << "Printer is not autoconfigurable";
      return PrinterSetupResult::kPrinterIsNotAutoconfigurable;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_DBUS_GENERIC:
      PRINTER_LOG(ERROR) << prefix << "Unknown D-Bus error";
      return PrinterSetupResult::kDbusError;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_DBUS_NO_REPLY:
      PRINTER_LOG(ERROR) << prefix << "D-Bus error - no reply";
      return PrinterSetupResult::kPrintscanmgrDbusNoReply;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_DBUS_TIMEOUT:
      PRINTER_LOG(ERROR) << prefix << "D-Bus error - timeout";
      return PrinterSetupResult::kDbusTimeout;
    // TODO(pmoy): handle new D-Bus encoding error here.
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_UNSPECIFIED:  // FALLTHROUGH
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_FATAL:  // FALLTHROUGH
    default:
      // We have no idea.  It must be fatal.
      PRINTER_LOG(ERROR) << prefix << "Unrecognized error: "
                         << printscanmgr::AddPrinterResult_Name(result);
      return PrinterSetupResult::kFatalError;
  }
}

// Searches in `ppd` for a command setting HP printer language. If the keyword
// is found, the function inserts after the command a line containing a path to
// Hplip plugin provided in `path` and returns true.
bool AddHplipPluginPathToPpdContent(std::string_view path, std::string& ppd) {
  constexpr char kHpPrinterLanguageKeyword[] = "*hpPrinterLanguage:";
  size_t pos = ppd.find(kHpPrinterLanguageKeyword);
  if (pos != std::string::npos) {
    pos = ppd.find('\n', pos + sizeof(kHpPrinterLanguageKeyword));
  }
  if (pos == std::string::npos) {
    return false;
  }
  ppd.insert(++pos,
             base::StrCat({"*chromeOSHplipPluginPath: \"", path, "\"\n"}));
  return true;
}

// Configures printers by downloading PPDs then adding them to CUPS through
// debugd.  This class must be used on the UI thread.
class PrinterConfigurerImpl : public PrinterConfigurer {
 public:
  PrinterConfigurerImpl(
      const ApplicationLocaleStorage* application_locale_storage,
      scoped_refptr<PpdProvider> ppd_provider,
      DlcserviceClient* dlc_service_client)
      : application_locale_storage_(CHECK_DEREF(application_locale_storage)),
        ppd_provider_(ppd_provider),
        dlc_service_client_(dlc_service_client) {
    DCHECK(ppd_provider_);
    DCHECK(dlc_service_client_);
  }

  PrinterConfigurerImpl(const PrinterConfigurerImpl&) = delete;
  PrinterConfigurerImpl& operator=(const PrinterConfigurerImpl&) = delete;

  ~PrinterConfigurerImpl() override = default;

  void SetUpPrinterInCups(const Printer& printer,
                          PrinterSetupCallback callback) override {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    DCHECK(!printer.id().empty());
    DCHECK(printer.HasUri());
    PRINTER_LOG(USER) << printer.id() << ": Printer setup requested for "
                      << printer.make_and_model();
    ppd_filename_.clear();

    if (!printer.IsIppEverywhere()) {
      if (!printer.ppd_reference().user_supplied_ppd_url.empty()) {
        // The PPD was provided by the user.
        ResolvePpd(printer, /*hplip_plugin_path=*/"", std::move(callback));
      } else {
        // The PPD was selected from our PPD Index. We have to check its license
        // to make sure it doesn't need any additional plugins before setup.
        PRINTER_LOG(DEBUG) << printer.id() << ": Check PPD license";
        ppd_provider_->ResolvePpdLicense(
            printer.ppd_reference().effective_make_and_model,
            base::BindOnce(&PrinterConfigurerImpl::ResolveLicenseDone,
                           weak_factory_.GetWeakPtr(), printer,
                           std::move(callback)));
      }
      return;
    }

    PRINTER_LOG(DEBUG) << printer.id() << ": Attempting driverless setup at "
                       << printer.uri().GetNormalized(
                              /*always_print_port=*/true);
    printscanmgr::CupsAddAutoConfiguredPrinterRequest request;
    request.set_name(printer.id());
    request.set_uri(printer.uri().GetNormalized(/*always_print_port=*/true));
    request.set_language(application_locale_storage_->Get());
    PrintscanmgrClient::Get()->CupsAddAutoConfiguredPrinter(
        std::move(request),
        base::BindOnce(&PrinterConfigurerImpl::OnAddedPrinter<
                           printscanmgr::CupsAddAutoConfiguredPrinterResponse>,
                       weak_factory_.GetWeakPtr(), printer,
                       std::move(callback)));
  }

 private:
  // Receive the callback from the printscanmgr daemon client once we attempt to
  // add the printer.
  template <typename T>
  void OnAddedPrinter(const Printer& printer,
                      PrinterSetupCallback cb,
                      std::optional<T> response) {
    // It's expected that the printscanmgr daemon posts callbacks on the UI
    // thread.
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    if (!response) {
      PRINTER_LOG(ERROR) << printer.id() << ": Null response to OnAddedPrinter";
      std::move(cb).Run(PrinterSetupResult::kFatalError);
      return;
    }

    PrinterSetupResult setup_result =
        PrinterSetupResultFromAddPrinterResult(printer, response->result());
    std::move(cb).Run(setup_result);
  }

  void AddPrinter(const Printer& printer,
                  const std::string& ppd_contents,
                  PrinterSetupCallback cb) {
    PRINTER_LOG(EVENT) << printer.id() << ": Attempting setup with PPD at "
                       << printer.uri().GetNormalized(
                              /*always_print_port=*/true);
    printscanmgr::CupsAddManuallyConfiguredPrinterRequest request;
    request.set_name(printer.id());
    request.set_uri(printer.uri().GetNormalized(/*always_print_port=*/true));
    request.set_ppd_contents(ppd_contents);
    request.set_language(application_locale_storage_->Get());
    PrintscanmgrClient::Get()->CupsAddManuallyConfiguredPrinter(
        std::move(request),
        base::BindOnce(
            &PrinterConfigurerImpl::OnAddedPrinter<
                printscanmgr::CupsAddManuallyConfiguredPrinterResponse>,
            weak_factory_.GetWeakPtr(), printer, std::move(cb)));
  }

  void ResolvePpdDone(const Printer& printer,
                      const std::string& hplip_plugin_path,
                      PrinterSetupCallback cb,
                      PpdProvider::CallbackResultCode result,
                      const std::string& ppd_contents,
                      const std::string& ppd_filename) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    ppd_filename_ = ppd_filename;

    PRINTER_LOG(EVENT) << printer.id() << " PPD Resolution Result: "
                       << PpdProvider::CallbackResultCodeName(result);
    switch (result) {
      case PpdProvider::SUCCESS:
        DCHECK(!ppd_contents.empty());
        {
          PRINTER_LOG(DEBUG)
              << printer.id() << " PPD filename: " << ppd_filename;
          // Use PpdLineReader to ungzip the content (if gzipped).
          auto reader = chromeos::PpdLineReader::Create(ppd_contents);
          std::string ppd = reader->RemainingContent();
          if (reader->Error()) {
            PRINTER_LOG(ERROR)
                << printer.id() << ": Error when reading/decompressing PPD";
          }
          if (!hplip_plugin_path.empty()) {
            if (!AddHplipPluginPathToPpdContent(hplip_plugin_path, ppd)) {
              PRINTER_LOG(ERROR) << printer.id()
                                 << ": Missing HP printer language in PPD file";
            }
          }
          AddPrinter(printer, ppd, std::move(cb));
        }
        break;
      case PpdProvider::CallbackResultCode::NOT_FOUND:
        std::move(cb).Run(PrinterSetupResult::kPpdNotFound);
        break;
      case PpdProvider::CallbackResultCode::SERVER_ERROR:
        std::move(cb).Run(PrinterSetupResult::kPpdUnretrievable);
        break;
      case PpdProvider::CallbackResultCode::INTERNAL_ERROR:
        std::move(cb).Run(PrinterSetupResult::kFatalError);
        break;
      case PpdProvider::CallbackResultCode::PPD_TOO_LARGE:
        std::move(cb).Run(PrinterSetupResult::kPpdTooLarge);
        break;
    }
  }

  void ResolvePpd(const Printer& printer,
                  const std::string& hplip_plugin_path,
                  PrinterSetupCallback cb) {
    PRINTER_LOG(DEBUG) << printer.id() << ": Looking up PPD";
    ppd_provider_->ResolvePpd(
        printer.ppd_reference(),
        base::BindOnce(&PrinterConfigurerImpl::ResolvePpdDone,
                       weak_factory_.GetWeakPtr(), printer, hplip_plugin_path,
                       std::move(cb)));
  }

  void ResolveLicenseDone(const Printer& printer,
                          PrinterSetupCallback cb,
                          PpdProvider::CallbackResultCode result,
                          const std::string& license_name) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    PRINTER_LOG(EVENT) << printer.id() << ": License resolution Result: "
                       << PpdProvider::CallbackResultCodeName(result);
    switch (result) {
      case PpdProvider::SUCCESS:
        break;
      case PpdProvider::CallbackResultCode::NOT_FOUND:
        std::move(cb).Run(PrinterSetupResult::kPpdNotFound);
        return;
      case PpdProvider::CallbackResultCode::SERVER_ERROR:
        std::move(cb).Run(PrinterSetupResult::kPpdUnretrievable);
        return;
      case PpdProvider::CallbackResultCode::INTERNAL_ERROR:
        std::move(cb).Run(PrinterSetupResult::kFatalError);
        return;
      case PpdProvider::CallbackResultCode::PPD_TOO_LARGE:
        std::move(cb).Run(PrinterSetupResult::kPpdTooLarge);
        return;
    }

    if (license_name == kEbuildWithHplipPlugins) {
      // Printers with this license require special plugin. We have to install
      // it before proceeding.
      PRINTER_LOG(DEBUG) << printer.id() << ": Installing hplip-plugin";
      dlcservice::InstallRequest install_request;
      install_request.set_id(kEbuildWithHplipPlugins);
      dlc_service_client_->Install(
          install_request,
          base::BindOnce(&PrinterConfigurerImpl::OnPluginInstallationComplete,
                         weak_factory_.GetWeakPtr(), printer, std::move(cb)),
          base::DoNothing());
    } else {
      // Proceed with PPD resolution.
      ResolvePpd(printer, /*hplip_plugin_path=*/"", std::move(cb));
    }
  }

  void OnPluginInstallationComplete(
      const Printer& printer,
      PrinterSetupCallback cb,
      const DlcserviceClient::InstallResult& result) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    if (result.root_path.empty()) {
      // Empty path of the plugin location means failure.
      PRINTER_LOG(ERROR) << printer.id() << ": Cannot install plugin "
                         << result.dlc_id << ": " << result.error;
      std::move(cb).Run(PrinterSetupResult::kComponentUnavailable);
    } else {
      // Plugin installed. We can proceed with PPD resolution.
      ResolvePpd(printer, result.root_path, std::move(cb));
    }
  }

  std::string GetLastPpdBasename() const override { return ppd_filename_; }

  const raw_ref<const ApplicationLocaleStorage> application_locale_storage_;
  scoped_refptr<PpdProvider> ppd_provider_;
  raw_ptr<DlcserviceClient> dlc_service_client_;
  std::string ppd_filename_;
  base::WeakPtrFactory<PrinterConfigurerImpl> weak_factory_{this};
};

}  // namespace

// static
std::string PrinterConfigurer::SetupFingerprint(const Printer& printer) {
  auto md5 = ash::printing::MakeMd5HasherForPrinterConfigurer();
  md5.Update(printer.id());
  md5.Update(printer.uri().GetNormalized(false));
  md5.Update(printer.ppd_reference().user_supplied_ppd_url);
  md5.Update(printer.ppd_reference().effective_make_and_model);
  char autoconf = printer.ppd_reference().autoconf ? 1 : 0;
  md5.Update(base::byte_span_from_ref(autoconf));
  return std::string(base::as_string_view(md5.Finish()));
}

// static
void PrinterConfigurer::RecordUsbPrinterSetupSource(
    UsbPrinterSetupSource source) {
  base::UmaHistogramEnumeration("Printing.CUPS.UsbSetupSource", source);
}

// static
std::unique_ptr<PrinterConfigurer> PrinterConfigurer::Create(
    const ApplicationLocaleStorage* application_locale_storage,
    scoped_refptr<PpdProvider> ppd_provider,
    DlcserviceClient* dlc_service_client) {
  return std::make_unique<PrinterConfigurerImpl>(
      application_locale_storage, ppd_provider, dlc_service_client);
}

// static
GURL PrinterConfigurer::GeneratePrinterEulaUrl(const std::string& license) {
  GURL eula_url(ash::kChromeUIOSCreditsURL);
  // Construct the URL with proper reference fragment.
  GURL::Replacements replacements;
  replacements.SetRefStr(license);
  return eula_url.ReplaceComponents(replacements);
}

std::string ResultCodeToMessage(const PrinterSetupResult result) {
  switch (result) {
    // Success.
    case PrinterSetupResult::kSuccess:
      return "Printer successfully configured.";
    case PrinterSetupResult::kEditSuccess:
      return "Printer successfully updated.";
    // Invalid configuration.
    case PrinterSetupResult::kNativePrintersNotAllowed:
      return "Unable to add or edit printer due to enterprise policy.";
    case PrinterSetupResult::kBadUri:
      return "Invalid URI.";
    case PrinterSetupResult::kInvalidPrinterUpdate:
      return "Requested printer changes would make printer unusable.";
    // Problem with a printer.
    case PrinterSetupResult::kPrinterUnreachable:
      return "Could not contact printer for configuration.";
    case PrinterSetupResult::kPrinterSentWrongResponse:
      return "Printer sent unexpected response.";
    case PrinterSetupResult::kPrinterIsNotAutoconfigurable:
      return "Printer is not autoconfigurable.";
    // Problem with a PPD file.
    case PrinterSetupResult::kPpdTooLarge:
      return "PPD is too large.";
    case PrinterSetupResult::kInvalidPpd:
      return "Provided PPD is invalid.";
    case PrinterSetupResult::kPpdNotFound:
      return "Could not locate requested PPD. Check printer configuration.";
    case PrinterSetupResult::kPpdUnretrievable:
      return "Could not retrieve PPD from server. Check Internet connection.";
    // Cannot load a required compomonent.
    case PrinterSetupResult::kComponentUnavailable:
      return "Could not install component.";
    // Problem with D-Bus.
    case PrinterSetupResult::kDbusError:
      return "D-Bus error occurred. Reboot required.";
    case PrinterSetupResult::kDbusNoReply:
      return "Deprecated.";
    case PrinterSetupResult::kDbusTimeout:
      return "Timed out trying to reach printscanmgr over D-Bus.";
    // Problem reported by OS.
    case PrinterSetupResult::kIoError:
      return "I/O error occurred.";
    case PrinterSetupResult::kMemoryAllocationError:
      return "Memory allocation error occurred.";
    // Unknown problem.
    case PrinterSetupResult::kFatalError:
      return "Unknown error occurred.";
    // Printer requires manual setup.
    case PrinterSetupResult::kManualSetupRequired:
      return "Printer requires manual setup.";
    case PrinterSetupResult::kPrinterRemoved:
      return "Printer was removed during the setup.";
    case PrinterSetupResult::kPrintscanmgrDbusNoReply:
      return "Couldn't talk to printscanmgr over D-Bus.";
    case PrinterSetupResult::kDebugdDbusNoReply:
      return "Couldn't talk to debugd over D-Bus.";
  }
}

}  // namespace ash
