// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/service/cloud_print/printer_job_handler.h"

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/md5.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/common/cloud_print/cloud_print_constants.h"
#include "chrome/common/cloud_print/cloud_print_helpers.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/service/cloud_print/cloud_print_service_helpers.h"
#include "chrome/service/cloud_print/job_status_updater.h"
#include "net/base/mime_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "printing/printing_utils.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

namespace cloud_print {

namespace {

base::subtle::Atomic32 g_total_jobs_started = 0;
base::subtle::Atomic32 g_total_jobs_done = 0;

enum PrinterJobHandlerEvent {
  JOB_HANDLER_CHECK_FOR_JOBS,
  JOB_HANDLER_START,
  JOB_HANDLER_PENDING_TASK,
  JOB_HANDLER_PRINTER_UPDATE,
  JOB_HANDLER_JOB_CHECK,
  JOB_HANDLER_JOB_STARTED,
  JOB_HANDLER_VALID_TICKET,
  JOB_HANDLER_DATA,
  JOB_HANDLER_SET_IN_PROGRESS,
  JOB_HANDLER_SET_START_PRINTING,
  JOB_HANDLER_START_SPOOLING,
  JOB_HANDLER_SPOOLED,
  JOB_HANDLER_JOB_COMPLETED,
  JOB_HANDLER_INVALID_TICKET,
  JOB_HANDLER_INVALID_DATA,
  JOB_HANDLER_MAX,
};

net::PartialNetworkTrafficAnnotationTag kPartialTrafficAnnotation =
    net::DefinePartialNetworkTrafficAnnotation("printer_job_handler",
                                               "cloud_print",
                                               R"(
        semantics {
          description:
            "Handles Cloud Print jobs for a particular printer, including "
            "connecting to printer, sending jobs, updating jobs, and getting "
            "status."
          trigger:
            "Automatic checking if printer is available, registering printer, "
            "and starting or continuing a printer task."
          data:
            "Cloud Print server URL, printer id, job details."
        })");

}  // namespace

PrinterJobHandler::PrinterInfoFromCloud::PrinterInfoFromCloud()
    : current_xmpp_timeout(0), pending_xmpp_timeout(0) {
}

PrinterJobHandler::PrinterInfoFromCloud::PrinterInfoFromCloud(
    const PrinterInfoFromCloud& other) = default;

PrinterJobHandler::PrinterJobHandler(
    const printing::PrinterBasicInfo& printer_info,
    const PrinterInfoFromCloud& printer_info_cloud,
    const GURL& cloud_print_server_url,
    PrintSystem* print_system,
    Delegate* delegate)
    : print_system_(print_system),
      printer_info_(printer_info),
      printer_info_cloud_(printer_info_cloud),
      cloud_print_server_url_(cloud_print_server_url),
      delegate_(delegate),
      local_job_id_(-1),
      next_json_data_handler_(NULL),
      next_data_handler_(NULL),
      print_thread_("Chrome_CloudPrintJobPrintThread"),
      job_handler_task_runner_(base::ThreadTaskRunnerHandle::Get()),
      shutting_down_(false),
      job_check_pending_(false),
      printer_update_pending_(true),
      task_in_progress_(false),
      weak_ptr_factory_(this) {
}

bool PrinterJobHandler::Initialize() {
  if (!print_system_->IsValidPrinter(printer_info_.printer_name))
    return false;

  printer_watcher_ = print_system_->CreatePrinterWatcher(
      printer_info_.printer_name);
  printer_watcher_->StartWatching(this);
  CheckForJobs(kJobFetchReasonStartup);
  return true;
}

std::string PrinterJobHandler::GetPrinterName() const {
  return printer_info_.printer_name;
}

void PrinterJobHandler::CheckForJobs(const std::string& reason) {
  VLOG(1) << "CP_CONNECTOR: Checking for jobs"
          << ", printer id: " << printer_info_cloud_.printer_id
          << ", reason: " << reason
          << ", task in progress: " << task_in_progress_;
  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                            JOB_HANDLER_CHECK_FOR_JOBS, JOB_HANDLER_MAX);
  job_fetch_reason_ = reason;
  job_check_pending_ = true;
  if (!task_in_progress_) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::Start, this));
  }
}

void PrinterJobHandler::Shutdown() {
  VLOG(1) << "CP_CONNECTOR: Shutting down printer job handler"
          << ", printer id: " << printer_info_cloud_.printer_id;
  Reset();
  shutting_down_ = true;
  while (!job_status_updater_list_.empty()) {
    // Calling Stop() will cause the OnJobCompleted to be called which will
    // remove the updater object from the list.
    job_status_updater_list_.front()->Stop();
  }
}

// CloudPrintURLFetcher::Delegate implementation.
CloudPrintURLFetcher::ResponseAction PrinterJobHandler::HandleRawResponse(
    const net::URLFetcher* source,
    const GURL& url,
    const net::URLRequestStatus& status,
    int response_code,
    const std::string& data) {
  // 415 (Unsupported media type) error while fetching data from the server
  // means data conversion error. Stop fetching process and mark job as error.
  if (next_data_handler_ == (&PrinterJobHandler::HandlePrintDataResponse) &&
      response_code == net::HTTP_UNSUPPORTED_MEDIA_TYPE) {
    VLOG(1) << "CP_CONNECTOR: Job failed (unsupported media type)";
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::JobFailed, this,
                                  JOB_DOWNLOAD_FAILED));
    return CloudPrintURLFetcher::STOP_PROCESSING;
  }
  return CloudPrintURLFetcher::CONTINUE_PROCESSING;
}

CloudPrintURLFetcher::ResponseAction PrinterJobHandler::HandleRawData(
    const net::URLFetcher* source,
    const GURL& url,
    const std::string& data) {
  if (!next_data_handler_)
    return CloudPrintURLFetcher::CONTINUE_PROCESSING;
  return (this->*next_data_handler_)(source, url, data);
}

CloudPrintURLFetcher::ResponseAction PrinterJobHandler::HandleJSONData(
    const net::URLFetcher* source,
    const GURL& url,
    const base::DictionaryValue* json_data,
    bool succeeded) {
  DCHECK(next_json_data_handler_);
  return (this->*next_json_data_handler_)(source, url, json_data, succeeded);
}

// Mark the job fetch as failed and check if other jobs can be printed
void PrinterJobHandler::OnRequestGiveUp() {
  if (job_queue_handler_.JobFetchFailed(job_details_.job_id_)) {
    VLOG(1) << "CP_CONNECTOR: Job failed to load (scheduling retry)";
    CheckForJobs(kJobFetchReasonFailure);
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::Stop, this));
  } else {
    VLOG(1) << "CP_CONNECTOR: Job failed (giving up after " <<
        kNumRetriesBeforeAbandonJob << " retries)";
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::JobFailed, this,
                                  JOB_DOWNLOAD_FAILED));
  }
}

CloudPrintURLFetcher::ResponseAction PrinterJobHandler::OnRequestAuthError() {
  // We got an Auth error and have no idea how long it will take to refresh
  // auth information (may take forever). We'll drop current request and
  // propagate this error to the upper level. After auth issues will be
  // resolved, GCP connector will restart.
  OnAuthError();
  return CloudPrintURLFetcher::STOP_PROCESSING;
}

std::string PrinterJobHandler::GetAuthHeader() {
  return GetCloudPrintAuthHeaderFromStore();
}

// JobStatusUpdater::Delegate implementation
bool PrinterJobHandler::OnJobCompleted(JobStatusUpdater* updater) {
  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                            JOB_HANDLER_JOB_COMPLETED, JOB_HANDLER_MAX);
  UMA_HISTOGRAM_LONG_TIMES("CloudPrint.PrintingTime",
                           base::Time::Now() - updater->start_time());
  base::subtle::NoBarrier_AtomicIncrement(&g_total_jobs_done, 1);
  job_queue_handler_.JobDone(job_details_.job_id_);

  for (JobStatusUpdaterList::iterator it = job_status_updater_list_.begin();
       it != job_status_updater_list_.end(); ++it) {
    if (it->get() == updater) {
      job_status_updater_list_.erase(it);
      return true;
    }
  }
  return false;
}

void PrinterJobHandler::OnAuthError() {
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(&PrinterJobHandler::Stop, this));
  if (delegate_)
    delegate_->OnAuthError();
}

void PrinterJobHandler::OnPrinterDeleted() {
  if (delegate_)
    delegate_->OnPrinterDeleted(printer_info_cloud_.printer_id);
}

void PrinterJobHandler::OnPrinterChanged() {
  printer_update_pending_ = true;
  if (!task_in_progress_) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::Start, this));
  }
}

void PrinterJobHandler::OnJobChanged() {
  // Some job on the printer changed. Loop through all our JobStatusUpdaters
  // and have them check for updates.
  for (const auto& it : job_status_updater_list_) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&JobStatusUpdater::UpdateStatus, it));
  }
}

void PrinterJobHandler::OnJobSpoolSucceeded(const PlatformJobId& job_id) {
  DCHECK(CurrentlyOnPrintThread());
  job_spooler_->AddRef();
  print_thread_.task_runner()->ReleaseSoon(FROM_HERE, job_spooler_.get());
  job_spooler_ = NULL;
  job_handler_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&PrinterJobHandler::JobSpooled, this, job_id));
}

void PrinterJobHandler::OnJobSpoolFailed() {
  DCHECK(CurrentlyOnPrintThread());
  job_spooler_->AddRef();
  print_thread_.task_runner()->ReleaseSoon(FROM_HERE, job_spooler_.get());
  job_spooler_ = NULL;
  VLOG(1) << "CP_CONNECTOR: Job failed (spool failed)";
  job_handler_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&PrinterJobHandler::JobFailed, this, JOB_FAILED));
}

// static
void PrinterJobHandler::ReportsStats() {
  base::subtle::Atomic32 started =
      base::subtle::NoBarrier_AtomicExchange(&g_total_jobs_started, 0);
  base::subtle::Atomic32 done =
      base::subtle::NoBarrier_AtomicExchange(&g_total_jobs_done, 0);
  UMA_HISTOGRAM_COUNTS_100("CloudPrint.JobsStartedPerInterval", started);
  UMA_HISTOGRAM_COUNTS_100("CloudPrint.JobsDonePerInterval", done);
}

PrinterJobHandler::~PrinterJobHandler() {
  if (printer_watcher_.get())
    printer_watcher_->StopWatching();
}

// Begin Response handlers
CloudPrintURLFetcher::ResponseAction
PrinterJobHandler::HandlePrinterUpdateResponse(
    const net::URLFetcher* source,
    const GURL& url,
    const base::DictionaryValue* json_data,
    bool succeeded) {
  VLOG(1) << "CP_CONNECTOR: Handling printer update response"
          << ", printer id: " << printer_info_cloud_.printer_id;
  // We are done here. Go to the Stop state
  VLOG(1) << "CP_CONNECTOR: Stopping printer job handler"
          << ", printer id: " << printer_info_cloud_.printer_id;
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(&PrinterJobHandler::Stop, this));
  return CloudPrintURLFetcher::STOP_PROCESSING;
}

CloudPrintURLFetcher::ResponseAction
PrinterJobHandler::HandleJobMetadataResponse(
    const net::URLFetcher* source,
    const GURL& url,
    const base::DictionaryValue* json_data,
    bool succeeded) {
  VLOG(1) << "CP_CONNECTOR: Handling job metadata response"
          << ", printer id: " << printer_info_cloud_.printer_id;
  bool job_available = false;
  if (succeeded) {
    std::vector<JobDetails> jobs =
        job_queue_handler_.GetJobsFromQueue(*json_data);
    if (!jobs.empty()) {
      if (jobs[0].time_remaining_.is_zero()) {
        job_available = true;
        job_details_ = jobs[0];
        job_start_time_ = base::Time::Now();
        base::subtle::NoBarrier_AtomicIncrement(&g_total_jobs_started, 1);
        UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                                  JOB_HANDLER_JOB_STARTED, JOB_HANDLER_MAX);
        SetNextDataHandler(&PrinterJobHandler::HandlePrintTicketResponse);
        request_ = CloudPrintURLFetcher::Create(kPartialTrafficAnnotation);
        if (print_system_->UseCddAndCjt()) {
          request_->StartGetRequest(
              CloudPrintURLFetcher::REQUEST_TICKET,
              GetUrlForJobCjt(cloud_print_server_url_, job_details_.job_id_,
                              job_fetch_reason_),
              this, kJobDataMaxRetryCount, std::string());
        } else {
          request_->StartGetRequest(
              CloudPrintURLFetcher::REQUEST_TICKET,
              GURL(job_details_.print_ticket_url_), this, kJobDataMaxRetryCount,
              std::string());
        }
      } else {
        job_available = false;
        base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(&PrinterJobHandler::RunScheduledJobCheck, this),
            jobs[0].time_remaining_);
      }
    }
  }

  if (!job_available) {
    // If no jobs are available, go to the Stop state.
    VLOG(1) << "CP_CONNECTOR: Stopping printer job handler"
            << ", printer id: " << printer_info_cloud_.printer_id;
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::Stop, this));
  }
  return CloudPrintURLFetcher::STOP_PROCESSING;
}

CloudPrintURLFetcher::ResponseAction
PrinterJobHandler::HandlePrintTicketResponse(const net::URLFetcher* source,
                                             const GURL& url,
                                             const std::string& data) {
  VLOG(1) << "CP_CONNECTOR: Handling print ticket response"
          << ", printer id: " << printer_info_cloud_.printer_id;
  std::string mime_type;
  source->GetResponseHeaders()->GetMimeType(&mime_type);
  if (print_system_->ValidatePrintTicket(printer_info_.printer_name, data,
                                         mime_type)) {
    UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                              JOB_HANDLER_VALID_TICKET, JOB_HANDLER_MAX);
    job_details_.print_ticket_ = data;
    job_details_.print_ticket_mime_type_ = mime_type;
    SetNextDataHandler(&PrinterJobHandler::HandlePrintDataResponse);
    request_ = CloudPrintURLFetcher::Create(kPartialTrafficAnnotation);
    std::string accept_headers = "Accept: ";
    accept_headers += print_system_->GetSupportedMimeTypes();
    request_->StartGetRequest(CloudPrintURLFetcher::REQUEST_DATA,
        GURL(job_details_.print_data_url_), this, kJobDataMaxRetryCount,
        accept_headers);
  } else {
    UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                              JOB_HANDLER_INVALID_TICKET, JOB_HANDLER_MAX);
    // The print ticket was not valid. We are done here.
    ValidatePrintTicketFailed();
  }
  return CloudPrintURLFetcher::STOP_PROCESSING;
}

CloudPrintURLFetcher::ResponseAction
PrinterJobHandler::HandlePrintDataResponse(const net::URLFetcher* source,
                                           const GURL& url,
                                           const std::string& data) {
  VLOG(1) << "CP_CONNECTOR: Handling print data response"
          << ", printer id: " << printer_info_cloud_.printer_id;
  if (base::CreateTemporaryFile(&job_details_.print_data_file_path_)) {
    UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent", JOB_HANDLER_DATA,
                              JOB_HANDLER_MAX);
    int ret = base::WriteFile(job_details_.print_data_file_path_,
                              data.c_str(), data.length());
    source->GetResponseHeaders()->GetMimeType(
        &job_details_.print_data_mime_type_);
    if (ret == static_cast<int>(data.length())) {
      UpdateJobStatus(PRINT_JOB_STATUS_IN_PROGRESS, JOB_SUCCESS);
      return CloudPrintURLFetcher::STOP_PROCESSING;
    }
  }
  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                            JOB_HANDLER_INVALID_DATA, JOB_HANDLER_MAX);

  // If we are here, then there was an error in saving the print data, bail out
  // here.
  VLOG(1) << "CP_CONNECTOR: Error saving print data"
          << ", printer id: " << printer_info_cloud_.printer_id;
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::BindOnce(&PrinterJobHandler::JobFailed, this, JOB_DOWNLOAD_FAILED));
  return CloudPrintURLFetcher::STOP_PROCESSING;
}

CloudPrintURLFetcher::ResponseAction
PrinterJobHandler::HandleInProgressStatusUpdateResponse(
    const net::URLFetcher* source,
    const GURL& url,
    const base::DictionaryValue* json_data,
    bool succeeded) {
  VLOG(1) << "CP_CONNECTOR: Handling success status update response"
          << ", printer id: " << printer_info_cloud_.printer_id;
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(&PrinterJobHandler::StartPrinting, this));
  return CloudPrintURLFetcher::STOP_PROCESSING;
}

CloudPrintURLFetcher::ResponseAction
PrinterJobHandler::HandleFailureStatusUpdateResponse(
    const net::URLFetcher* source,
    const GURL& url,
    const base::DictionaryValue* json_data,
    bool succeeded) {
  VLOG(1) << "CP_CONNECTOR: Handling failure status update response"
          << ", printer id: " << printer_info_cloud_.printer_id;
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(&PrinterJobHandler::Stop, this));
  return CloudPrintURLFetcher::STOP_PROCESSING;
}

void PrinterJobHandler::Start() {
  VLOG(1) << "CP_CONNECTOR: Starting printer job handler"
          << ", printer id: " << printer_info_cloud_.printer_id
          << ", task in progress: " << task_in_progress_;
  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                            JOB_HANDLER_START, JOB_HANDLER_MAX);
  if (task_in_progress_) {
    // Multiple Starts can get posted because of multiple notifications
    // We want to ignore the other ones that happen when a task is in progress.
    return;
  }
  Reset();
  if (!shutting_down_) {
    // Check if we have work to do.
    if (HavePendingTasks()) {
      UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                                JOB_HANDLER_PENDING_TASK, JOB_HANDLER_MAX);
      if (!task_in_progress_ && printer_update_pending_) {
        UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                                  JOB_HANDLER_PRINTER_UPDATE, JOB_HANDLER_MAX);
        printer_update_pending_ = false;
        task_in_progress_ = UpdatePrinterInfo();
        VLOG(1) << "CP_CONNECTOR: Changed task in progress"
                << ", printer id: " << printer_info_cloud_.printer_id
                << ", task in progress: " << task_in_progress_;
      }
      if (!task_in_progress_ && job_check_pending_) {
        UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                                  JOB_HANDLER_JOB_CHECK, JOB_HANDLER_MAX);
        task_in_progress_ = true;
        VLOG(1) << "CP_CONNECTOR: Changed task in progress"
                ", printer id: " << printer_info_cloud_.printer_id
                << ", task in progress: " << task_in_progress_;
        job_check_pending_ = false;
        // We need to fetch any pending jobs for this printer
        SetNextJSONHandler(&PrinterJobHandler::HandleJobMetadataResponse);
        request_ = CloudPrintURLFetcher::Create(kPartialTrafficAnnotation);
        request_->StartGetRequest(
            CloudPrintURLFetcher::REQUEST_JOB_FETCH,
            GetUrlForJobFetch(
                cloud_print_server_url_, printer_info_cloud_.printer_id,
                job_fetch_reason_),
            this,
            kCloudPrintAPIMaxRetryCount,
            std::string());
        last_job_fetch_time_ = base::TimeTicks::Now();
        VLOG(1) << "CP_CONNECTOR: Last job fetch time"
                << ", printer name: " << printer_info_.printer_name.c_str()
                << ", timestamp: " << last_job_fetch_time_.ToInternalValue();
        job_fetch_reason_.clear();
      }
    }
  }
}

void PrinterJobHandler::Stop() {
  VLOG(1) << "CP_CONNECTOR: Stopping printer job handler"
          << ", printer id: " << printer_info_cloud_.printer_id;
  task_in_progress_ = false;
  VLOG(1) << "CP_CONNECTOR: Changed task in progress"
          << ", printer id: " << printer_info_cloud_.printer_id
          << ", task in progress: " << task_in_progress_;
  Reset();
  if (HavePendingTasks()) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::Start, this));
  }
}

void PrinterJobHandler::StartPrinting() {
  VLOG(1) << "CP_CONNECTOR: Starting printing"
          << ", printer id: " << printer_info_cloud_.printer_id;
  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                            JOB_HANDLER_SET_START_PRINTING, JOB_HANDLER_MAX);
  // We are done with the request object for now.
  request_ = NULL;
  if (!shutting_down_) {
#if defined(OS_WIN)
    print_thread_.init_com_with_mta(true);
#endif
    if (!print_thread_.Start()) {
      VLOG(1) << "CP_CONNECTOR: Failed to start print thread"
              << ", printer id: " << printer_info_cloud_.printer_id;
      JobFailed(JOB_FAILED);
    } else {
      print_thread_.task_runner()->PostTask(
          FROM_HERE, base::BindOnce(&PrinterJobHandler::DoPrint, this,
                                    job_details_, printer_info_.printer_name));
    }
  }
}

void PrinterJobHandler::Reset() {
  job_details_.Clear();
  request_ = NULL;
  print_thread_.Stop();
}

void PrinterJobHandler::UpdateJobStatus(PrintJobStatus status,
                                        PrintJobError error) {
  VLOG(1) << "CP_CONNECTOR: Updating job status"
          << ", printer id: " << printer_info_cloud_.printer_id
          << ", job id: " << job_details_.job_id_
          << ", job status: " << status;
  if (shutting_down_) {
    VLOG(1) << "CP_CONNECTOR: Job status update aborted (shutting down)"
            << ", printer id: " << printer_info_cloud_.printer_id
            << ", job id: " << job_details_.job_id_;
    return;
  }
  if (job_details_.job_id_.empty()) {
    VLOG(1) << "CP_CONNECTOR: Job status update aborted (empty job id)"
            << ", printer id: " << printer_info_cloud_.printer_id;
    return;
  }

  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobStatus", error, JOB_MAX);

  if (error == JOB_SUCCESS) {
    DCHECK_EQ(status, PRINT_JOB_STATUS_IN_PROGRESS);
    UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                              JOB_HANDLER_SET_IN_PROGRESS, JOB_HANDLER_MAX);
    SetNextJSONHandler(
        &PrinterJobHandler::HandleInProgressStatusUpdateResponse);
  } else {
    SetNextJSONHandler(
        &PrinterJobHandler::HandleFailureStatusUpdateResponse);
  }
  request_ = CloudPrintURLFetcher::Create(kPartialTrafficAnnotation);
  request_->StartGetRequest(
      CloudPrintURLFetcher::REQUEST_UPDATE_JOB,
      GetUrlForJobStatusUpdate(cloud_print_server_url_, job_details_.job_id_,
                               status, error),
      this, kCloudPrintAPIMaxRetryCount, std::string());
}

void PrinterJobHandler::RunScheduledJobCheck() {
  CheckForJobs(kJobFetchReasonRetry);
}

void PrinterJobHandler::SetNextJSONHandler(JSONDataHandler handler) {
  next_json_data_handler_ = handler;
  next_data_handler_ = NULL;
}

void PrinterJobHandler::SetNextDataHandler(DataHandler handler) {
  next_data_handler_ = handler;
  next_json_data_handler_ = NULL;
}

void PrinterJobHandler::JobFailed(PrintJobError error) {
  VLOG(1) << "CP_CONNECTOR: Job failed"
          << ", printer id: " << printer_info_cloud_.printer_id
          << ", job id: " << job_details_.job_id_
          << ", error: " << error;
  if (!shutting_down_) {
    UpdateJobStatus(PRINT_JOB_STATUS_ERROR, error);
    // This job failed, but others may be pending.  Schedule a check.
    job_check_pending_ = true;
    job_fetch_reason_ = kJobFetchReasonFailure;
  }
}

void PrinterJobHandler::JobSpooled(PlatformJobId local_job_id) {
  VLOG(1) << "CP_CONNECTOR: Job spooled"
          << ", printer id: " << printer_info_cloud_.printer_id
          << ", job id: " << local_job_id;
  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent", JOB_HANDLER_SPOOLED,
                            JOB_HANDLER_MAX);
  UMA_HISTOGRAM_LONG_TIMES("CloudPrint.SpoolingTime",
                           base::Time::Now() - spooling_start_time_);
  if (shutting_down_)
    return;

  local_job_id_ = local_job_id;
  print_thread_.Stop();

  // The print job has been spooled locally. We now need to create an object
  // that monitors the status of the job and updates the server.
  scoped_refptr<JobStatusUpdater> job_status_updater(new JobStatusUpdater(
      printer_info_.printer_name, job_details_.job_id_, local_job_id_,
      cloud_print_server_url_, print_system_.get(), this,
      kPartialTrafficAnnotation));
  job_status_updater_list_.push_back(job_status_updater);
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::BindOnce(&JobStatusUpdater::UpdateStatus, job_status_updater));

  CheckForJobs(kJobFetchReasonQueryMore);

  VLOG(1) << "CP_CONNECTOR: Stopping printer job handler"
          << ", printer id: " << printer_info_cloud_.printer_id;
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(&PrinterJobHandler::Stop, this));
}

bool PrinterJobHandler::UpdatePrinterInfo() {
  if (!printer_watcher_.get()) {
    LOG(ERROR) << "CP_CONNECTOR: Printer watcher is missing."
               << " Check printer server url for printer id: "
               << printer_info_cloud_.printer_id;
    return false;
  }

  VLOG(1) << "CP_CONNECTOR: Updating printer info"
          << ", printer id: " << printer_info_cloud_.printer_id;
  // We need to update the parts of the printer info that have changed
  // (could be printer name, description, status or capabilities).
  // First asynchronously fetch the capabilities.
  printing::PrinterBasicInfo printer_info;
  printer_watcher_->GetCurrentPrinterInfo(&printer_info);

  // Asynchronously fetch the printer caps and defaults. The story will
  // continue in OnReceivePrinterCaps.
  print_system_->GetPrinterCapsAndDefaults(
      printer_info.printer_name.c_str(),
      base::Bind(&PrinterJobHandler::OnReceivePrinterCaps,
                 weak_ptr_factory_.GetWeakPtr()));

  // While we are waiting for the data, pretend we have work to do and return
  // true.
  return true;
}

bool PrinterJobHandler::HavePendingTasks() {
  return (job_check_pending_ || printer_update_pending_);
}

void PrinterJobHandler::ValidatePrintTicketFailed() {
  if (!shutting_down_) {
    LOG(ERROR) << "CP_CONNECTOR: Failed validating print ticket"
               << ", printer name: " << printer_info_.printer_name
               << ", job id: " << job_details_.job_id_;
    JobFailed(JOB_VALIDATE_TICKET_FAILED);
  }
}

void PrinterJobHandler::OnReceivePrinterCaps(
    bool succeeded,
    const std::string& printer_name,
    const printing::PrinterCapsAndDefaults& caps_and_defaults) {
  printing::PrinterBasicInfo printer_info;
  if (printer_watcher_.get())
    printer_watcher_->GetCurrentPrinterInfo(&printer_info);

  std::string post_data;
  std::string mime_boundary = net::GenerateMimeMultipartBoundary();

  if (succeeded) {
    std::string caps_hash =
        base::MD5String(caps_and_defaults.printer_capabilities);
    if (caps_hash != printer_info_cloud_.caps_hash) {
      // Hashes don't match, we need to upload new capabilities (the defaults
      // go for free along with the capabilities)
      printer_info_cloud_.caps_hash = caps_hash;
      if (caps_and_defaults.caps_mime_type == kContentTypeJSON) {
        DCHECK(print_system_->UseCddAndCjt());
        net::AddMultipartValueForUpload(kUseCDD, "true", mime_boundary,
                                        std::string(), &post_data);
      }
      net::AddMultipartValueForUpload(kPrinterCapsValue,
          caps_and_defaults.printer_capabilities, mime_boundary,
          caps_and_defaults.caps_mime_type, &post_data);
      net::AddMultipartValueForUpload(kPrinterDefaultsValue,
          caps_and_defaults.printer_defaults, mime_boundary,
          caps_and_defaults.defaults_mime_type, &post_data);
      net::AddMultipartValueForUpload(kPrinterCapsHashValue,
          caps_hash, mime_boundary, std::string(), &post_data);
    }
  } else {
    LOG(ERROR) << "Failed to get printer caps and defaults"
               << ", printer name: " << printer_name;
  }

  std::string tags_hash = GetHashOfPrinterInfo(printer_info);
  if (tags_hash != printer_info_cloud_.tags_hash) {
    printer_info_cloud_.tags_hash = tags_hash;
    post_data += GetPostDataForPrinterInfo(printer_info, mime_boundary);
    // Remove all the existing proxy tags.
    std::string cp_tag_wildcard(kCloudPrintServiceProxyTagPrefix);
    cp_tag_wildcard += ".*";
    net::AddMultipartValueForUpload(kPrinterRemoveTagValue,
        cp_tag_wildcard, mime_boundary, std::string(), &post_data);
  }

  if (printer_info.printer_name != printer_info_.printer_name) {
    net::AddMultipartValueForUpload(kPrinterNameValue,
        printer_info.printer_name, mime_boundary, std::string(), &post_data);
  }
  if (printer_info.printer_description != printer_info_.printer_description) {
    net::AddMultipartValueForUpload(kPrinterDescValue,
      printer_info.printer_description, mime_boundary,
      std::string(), &post_data);
  }
  if (printer_info.printer_status != printer_info_.printer_status) {
    net::AddMultipartValueForUpload(kPrinterStatusValue,
        base::IntToString(printer_info.printer_status), mime_boundary,
        std::string(), &post_data);
  }

  // Add local_settings with a current XMPP ping interval.
  if (printer_info_cloud_.pending_xmpp_timeout != 0) {
    DCHECK(kMinXmppPingTimeoutSecs <= printer_info_cloud_.pending_xmpp_timeout);
    net::AddMultipartValueForUpload(kPrinterLocalSettingsValue,
        base::StringPrintf(kUpdateLocalSettingsXmppPingFormat,
            printer_info_cloud_.current_xmpp_timeout),
        mime_boundary, std::string(), &post_data);
  }

  printer_info_ = printer_info;
  if (!post_data.empty()) {
    net::AddMultipartFinalDelimiterForUpload(mime_boundary, &post_data);
    std::string mime_type("multipart/form-data; boundary=");
    mime_type += mime_boundary;
    SetNextJSONHandler(&PrinterJobHandler::HandlePrinterUpdateResponse);
    request_ = CloudPrintURLFetcher::Create(kPartialTrafficAnnotation);
    request_->StartPostRequest(
        CloudPrintURLFetcher::REQUEST_UPDATE_PRINTER,
        GetUrlForPrinterUpdate(
            cloud_print_server_url_, printer_info_cloud_.printer_id),
        this,
        kCloudPrintAPIMaxRetryCount,
        mime_type,
        post_data,
        std::string());
  } else {
    // We are done here. Go to the Stop state
    VLOG(1) << "CP_CONNECTOR: Stopping printer job handler"
            << ", printer name: " << printer_name;
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&PrinterJobHandler::Stop, this));
  }
}

void PrinterJobHandler::DoPrint(const JobDetails& job_details,
                                const std::string& printer_name) {
  DCHECK(CurrentlyOnPrintThread());
  job_spooler_ = print_system_->CreateJobSpooler();
  UMA_HISTOGRAM_LONG_TIMES("CloudPrint.PrepareTime",
                           base::Time::Now() - job_start_time_);
  DCHECK(job_spooler_.get());

  base::string16 document_name =
      job_details.job_title_.empty()
          ? l10n_util::GetStringUTF16(IDS_DEFAULT_PRINT_DOCUMENT_TITLE)
          : base::UTF8ToUTF16(job_details.job_title_);

  document_name = printing::FormatDocumentTitleWithOwner(
      base::UTF8ToUTF16(job_details.job_owner_), document_name);

  UMA_HISTOGRAM_ENUMERATION("CloudPrint.JobHandlerEvent",
                            JOB_HANDLER_START_SPOOLING, JOB_HANDLER_MAX);
  spooling_start_time_ = base::Time::Now();
  if (!job_spooler_->Spool(job_details.print_ticket_,
                           job_details.print_ticket_mime_type_,
                           job_details.print_data_file_path_,
                           job_details.print_data_mime_type_,
                           printer_name,
                           base::UTF16ToUTF8(document_name),
                           job_details.tags_,
                           this)) {
    OnJobSpoolFailed();
  }
}

bool PrinterJobHandler::CurrentlyOnPrintThread() const {
  return print_thread_.task_runner()->BelongsToCurrentThread();
}

}  // namespace cloud_print
