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

#include "components/sync/engine/net/sync_server_connection_manager.h"

#include <stdint.h>

#include <utility>

#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "components/sync/base/features.h"
#include "components/sync/engine/cancelation_signal.h"
#include "components/sync/engine/net/http_post_provider.h"
#include "components/sync/engine/net/http_post_provider_factory.h"
#include "net/base/net_errors.h"
#include "net/http/http_status_code.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

namespace syncer {
namespace {

// This provides HTTP Post functionality through the interface provided
// by the application hosting the syncer backend.
class Connection : public CancelationSignal::Observer {
 public:
  // All pointers must not be null and must outlive this object.
  Connection(HttpPostProviderFactory* factory,
             CancelationSignal* cancelation_signal);

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

  ~Connection() override;

  HttpResponse PostRequestAndDownloadResponse(const GURL& connection_url,
                                              const std::string& access_token,
                                              const std::string& payload,
                                              std::string* buffer_out);

  // CancelationSignal::Observer overrides.
  void OnCancelationSignalReceived() override;

 private:
  // Pointer to the factory we use for creating HttpPostProviders. We do not
  // own `factory_`.
  const raw_ptr<HttpPostProviderFactory> factory_;

  // Cancelation signal is signalled when engine shuts down. Current blocking
  // operation should be aborted.
  const raw_ptr<CancelationSignal> cancelation_signal_;

  scoped_refptr<HttpPostProvider> const post_provider_;
};

Connection::Connection(HttpPostProviderFactory* factory,
                       CancelationSignal* cancelation_signal)
    : factory_(factory),
      cancelation_signal_(cancelation_signal),
      post_provider_(factory_->Create()) {
  DCHECK(factory);
  DCHECK(cancelation_signal);
  DCHECK(post_provider_);
}

Connection::~Connection() = default;

HttpResponse Connection::PostRequestAndDownloadResponse(
    const GURL& sync_request_url,
    const std::string& access_token,
    const std::string& payload,
    std::string* buffer_out) {
  post_provider_->SetURL(sync_request_url);

  if (!access_token.empty()) {
    net::HttpRequestHeaders headers;
    headers.SetHeader("Authorization", "Bearer " + access_token);
    post_provider_->SetExtraRequestHeaders(headers);
  }

  // Must be octet-stream, or the payload may be parsed for a cookie.
  post_provider_->SetPostPayload("application/octet-stream", payload.length(),
                                 payload.data());

  // Issue the POST, blocking until it finishes.
  if (!cancelation_signal_->TryRegisterHandler(this)) {
    // Return early because cancelation signal was signaled.
    return HttpResponse::ForUnspecifiedError();
  }
  absl::Cleanup auto_unregister = [this] {
    cancelation_signal_->UnregisterHandler(this);
  };

  int net_error_code = 0;
  int http_status_code = 0;
  if (!post_provider_->MakeSynchronousPost(&net_error_code,
                                           &http_status_code)) {
    DCHECK_NE(net_error_code, net::OK);
    DVLOG(1) << "Http POST failed, error returns: " << net_error_code;
    return HttpResponse::ForNetError(net_error_code);
  }

  // We got a server response, copy over response codes and content.
  HttpResponse response = HttpResponse::ForHttpStatusCode(http_status_code);
  response.content_length = post_provider_->GetResponseContentLength();

  // Write the content into the buffer.
  buffer_out->assign(post_provider_->GetResponseContent(),
                     post_provider_->GetResponseContentLength());
  return response;
}

void Connection::OnCancelationSignalReceived() {
  DCHECK(post_provider_);
  post_provider_->Abort();
}

}  // namespace

SyncServerConnectionManager::SyncServerConnectionManager(
    const GURL& sync_request_url,
    std::unique_ptr<HttpPostProviderFactory> factory,
    CancelationSignal* cancelation_signal)
    : sync_request_url_(sync_request_url),
      post_provider_factory_(std::move(factory)),
      cancelation_signal_(cancelation_signal) {
  DCHECK(post_provider_factory_);
  DCHECK(cancelation_signal_);
}

SyncServerConnectionManager::~SyncServerConnectionManager() = default;

HttpResponse SyncServerConnectionManager::PostBuffer(
    const std::string& buffer_in,
    std::string* buffer_out,
    const signin::AccessTokenInfo& access_token_info) {
  const bool is_access_token_valid = IsAccessTokenInfoValid(access_token_info);
  base::UmaHistogramBoolean("Sync.URLFetchAccessToken", is_access_token_valid);

  if (!is_access_token_valid) {
    ClearCachedAccessToken();
    return HttpResponse::ForHttpStatusCode(net::HTTP_UNAUTHORIZED);
  }

  if (cancelation_signal_->IsSignalled()) {
    return HttpResponse::ForUnspecifiedError();
  }

  auto connection = std::make_unique<Connection>(post_provider_factory_.get(),
                                                 cancelation_signal_);

  // Note that the post may be aborted by now, which will just cause Init to
  // fail with CONNECTION_UNAVAILABLE.
  HttpResponse http_response = connection->PostRequestAndDownloadResponse(
      sync_request_url_, access_token_info.token, buffer_in, buffer_out);

  if (http_response.server_status == HttpResponse::SYNC_AUTH_ERROR) {
    ClearCachedAccessToken();
  }

  return http_response;
}

}  // namespace syncer
