// 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.

#include "components/invalidation/impl/per_user_topic_subscription_request.h"

#include <memory>
#include <optional>
#include <string>

#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

using net::HttpRequestHeaders;

namespace invalidation {

namespace {

const char kPublicTopicNameKey[] = "publicTopicName";
const char kPrivateTopicNameKey[] = "privateTopicName";

bool IsNetworkError(int net_error) {
  // Note: ERR_HTTP_RESPONSE_CODE_FAILURE isn't a real network error - it
  // indicates that the network request itself succeeded, but we received an
  // HTTP error. The alternative to special-casing this error would be to call
  // SetAllowHttpErrorResults(true) on the SimpleUrlLoader, but that also means
  // we'd download the response body in case of HTTP errors, which we don't
  // need.
  return net_error != net::OK &&
         net_error != net::ERR_HTTP_RESPONSE_CODE_FAILURE;
}

// Subscription status values for UMA_HISTOGRAM.
enum class SubscriptionStatus {
  kSuccess = 0,
  kNetworkFailure = 1,
  kHttpFailure = 2,
  kParsingFailure = 3,
  kFailure = 4,
  kMaxValue = kFailure,
};

void RecordRequestStatus(SubscriptionStatus status,
                         PerUserTopicSubscriptionRequest::RequestType type,
                         const std::string& topic,
                         int net_error = net::OK,
                         int response_code = 200) {
  switch (type) {
    case PerUserTopicSubscriptionRequest::RequestType::kSubscribe: {
      base::UmaHistogramEnumeration(
          "FCMInvalidations.SubscriptionRequestStatus", status);
      break;
    }
    case PerUserTopicSubscriptionRequest::RequestType::kUnsubscribe: {
      base::UmaHistogramEnumeration(
          "FCMInvalidations.UnsubscriptionRequestStatus", status);
      break;
    }
  }
  if (type != PerUserTopicSubscriptionRequest::RequestType::kSubscribe) {
    return;
  }

  if (IsNetworkError(net_error)) {
    // Tracks the cases, when request fails due to network error.
    base::UmaHistogramSparse("FCMInvalidations.FailedSubscriptionsErrorCode",
                             -net_error);
    DVLOG(1) << "Subscription request failed with error: " << net_error << ": "
             << net::ErrorToString(net_error);
  } else {
    // Log a histogram to track response success vs. failure rates.
    base::UmaHistogramSparse("FCMInvalidations.SubscriptionResponseCode",
                             response_code);
  }
}

}  // namespace

PerUserTopicSubscriptionRequest::PerUserTopicSubscriptionRequest() = default;

PerUserTopicSubscriptionRequest::~PerUserTopicSubscriptionRequest() = default;

void PerUserTopicSubscriptionRequest::Start(
    CompletedCallback callback,
    network::mojom::URLLoaderFactory* loader_factory) {
  DCHECK(request_completed_callback_.is_null()) << "Request already running!";
  request_completed_callback_ = std::move(callback);

  simple_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
      loader_factory,
      base::BindOnce(&PerUserTopicSubscriptionRequest::OnURLFetchComplete,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PerUserTopicSubscriptionRequest::OnURLFetchComplete(
    std::optional<std::string> response_body) {
  int response_code = 0;
  if (simple_loader_->ResponseInfo() &&
      simple_loader_->ResponseInfo()->headers) {
    response_code = simple_loader_->ResponseInfo()->headers->response_code();
  }
  OnURLFetchCompleteInternal(simple_loader_->NetError(), response_code,
                             std::move(response_body));
  // Potentially dead after the above invocation; nothing to do except return.
}

void PerUserTopicSubscriptionRequest::OnURLFetchCompleteInternal(
    int net_error,
    int response_code,
    std::optional<std::string> response_body) {
  if (IsNetworkError(net_error)) {
    RecordRequestStatus(SubscriptionStatus::kNetworkFailure, type_, topic_,
                        net_error, response_code);
    RunCompletedCallbackAndMaybeDie(Status(StatusCode::FAILED, "Network Error"),
                                    std::string());
    // Potentially dead after the above invocation; nothing to do except return.
    return;
  }

  if (response_code != net::HTTP_OK) {
    StatusCode status = StatusCode::FAILED;
    if (response_code == net::HTTP_UNAUTHORIZED) {
      status = StatusCode::AUTH_FAILURE;
    } else if (response_code >= 400 && response_code <= 499) {
      status = StatusCode::FAILED_NON_RETRIABLE;
    }
    RecordRequestStatus(SubscriptionStatus::kHttpFailure, type_, topic_,
                        net_error, response_code);
    RunCompletedCallbackAndMaybeDie(
        Status(status, base::StringPrintf("HTTP Error: %d", response_code)),
        std::string());
    // Potentially dead after the above invocation; nothing to do except return.
    return;
  }

  if (type_ == RequestType::kUnsubscribe) {
    // No response body expected for DELETE requests.
    RecordRequestStatus(SubscriptionStatus::kSuccess, type_, topic_, net_error,
                        response_code);
    RunCompletedCallbackAndMaybeDie(Status(StatusCode::SUCCESS, std::string()),
                                    std::string());
    // Potentially dead after the above invocation; nothing to do except return.
    return;
  }

  if (!response_body || response_body->empty()) {
    RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_,
                        net_error, response_code);
    RunCompletedCallbackAndMaybeDie(Status(StatusCode::FAILED, "Body missing"),
                                    std::string());
    // Potentially dead after the above invocation; nothing to do except return.
    return;
  }

  std::optional<base::DictValue> response_dict =
      base::JSONReader::ReadDict(*response_body, base::JSON_PARSE_RFC);
  if (!response_dict) {
    RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_);
    RunCompletedCallbackAndMaybeDie(
        Status(StatusCode::FAILED, "Body parse error"), std::string());
    // Potentially dead after the above invocation; nothing to do except return.
    return;
  }

  const std::string* topic_name =
      response_dict->FindBool("isPublic").value_or(false)
          ? response_dict->FindString(kPublicTopicNameKey)
          : response_dict->FindString(kPrivateTopicNameKey);

  if (!topic_name) {
    RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_);
    RunCompletedCallbackAndMaybeDie(
        Status(StatusCode::FAILED, "Missing topic name"), std::string());
    // Potentially dead after the above invocation; nothing to do except return.
    return;
  }

  RecordRequestStatus(SubscriptionStatus::kSuccess, type_, topic_);
  RunCompletedCallbackAndMaybeDie(Status(StatusCode::SUCCESS, std::string()),
                                  *topic_name);
  // Potentially dead after the above invocation; nothing to do except return.
}

void PerUserTopicSubscriptionRequest::RunCompletedCallbackAndMaybeDie(
    Status status,
    std::string topic_name) {
  std::move(request_completed_callback_)
      .Run(std::move(status), std::move(topic_name));
}

PerUserTopicSubscriptionRequest::Builder::Builder() = default;
PerUserTopicSubscriptionRequest::Builder::~Builder() = default;

std::unique_ptr<PerUserTopicSubscriptionRequest>
PerUserTopicSubscriptionRequest::Builder::Build() const {
  DCHECK(!scope_.empty());
  auto request = base::WrapUnique(new PerUserTopicSubscriptionRequest());

  std::string url;
  switch (type_) {
    case RequestType::kSubscribe:
      url = base::StringPrintf(
          "%s/v1/perusertopics/%s/rel/topics/?subscriber_token=%s",
          scope_.c_str(), project_id_.c_str(), instance_id_token_.c_str());
      break;
    case RequestType::kUnsubscribe:
      std::string public_param;
      if (topic_is_public_) {
        public_param = "subscription.is_public=true&";
      }
      url = base::StringPrintf(
          "%s/v1/perusertopics/%s/rel/topics/%s?%ssubscriber_token=%s",
          scope_.c_str(), project_id_.c_str(), topic_.c_str(),
          public_param.c_str(), instance_id_token_.c_str());
      break;
  }
  GURL full_url(url);

  DCHECK(full_url.is_valid());

  request->url_ = full_url;
  request->type_ = type_;
  request->topic_ = topic_;

  std::string body;
  if (type_ == RequestType::kSubscribe) {
    body = BuildBody();
  }
  net::HttpRequestHeaders headers = BuildHeaders();
  request->simple_loader_ = BuildURLFetcher(headers, body, full_url);

  // Log the request for debugging network issues.
  DVLOG(1) << "Building a subscription request to " << full_url << ":\n"
           << headers.ToString() << "\n"
           << body;
  return request;
}

PerUserTopicSubscriptionRequest::Builder&
PerUserTopicSubscriptionRequest::Builder::SetInstanceIdToken(
    const std::string& token) {
  instance_id_token_ = token;
  return *this;
}

PerUserTopicSubscriptionRequest::Builder&
PerUserTopicSubscriptionRequest::Builder::SetScope(const std::string& scope) {
  scope_ = scope;
  return *this;
}

PerUserTopicSubscriptionRequest::Builder&
PerUserTopicSubscriptionRequest::Builder::SetAuthenticationHeader(
    const std::string& auth_header) {
  auth_header_ = auth_header;
  return *this;
}

PerUserTopicSubscriptionRequest::Builder&
PerUserTopicSubscriptionRequest::Builder::SetPublicTopicName(
    const Topic& topic) {
  topic_ = topic;
  return *this;
}

PerUserTopicSubscriptionRequest::Builder&
PerUserTopicSubscriptionRequest::Builder::SetProjectId(
    const std::string& project_id) {
  project_id_ = project_id;
  return *this;
}

PerUserTopicSubscriptionRequest::Builder&
PerUserTopicSubscriptionRequest::Builder::SetType(RequestType type) {
  type_ = type;
  return *this;
}

PerUserTopicSubscriptionRequest::Builder&
PerUserTopicSubscriptionRequest::Builder::SetTopicIsPublic(
    bool topic_is_public) {
  topic_is_public_ = topic_is_public;
  return *this;
}

HttpRequestHeaders PerUserTopicSubscriptionRequest::Builder::BuildHeaders()
    const {
  HttpRequestHeaders headers;
  if (!auth_header_.empty()) {
    headers.SetHeader(HttpRequestHeaders::kAuthorization, auth_header_);
  }
  return headers;
}

std::string PerUserTopicSubscriptionRequest::Builder::BuildBody() const {
  base::DictValue request;

  request.Set("public_topic_name", topic_);
  if (topic_is_public_)
    request.Set("is_public", true);

  std::string request_json;
  bool success = base::JSONWriter::Write(request, &request_json);
  DCHECK(success);
  return request_json;
}

std::unique_ptr<network::SimpleURLLoader>
PerUserTopicSubscriptionRequest::Builder::BuildURLFetcher(
    const HttpRequestHeaders& headers,
    const std::string& body,
    const GURL& url) const {
  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("per_user_topic_registration_request",
                                          R"(
        semantics {
          sender:
            "Subscribe for listening to the specific user topic"
          description:
            "This request subscribes the client for receiving FCM messages for"
            "the concrete user topic."
          trigger:
            "Subscription takes place only once per profile per topic. "
          data:
            "An OAuth2 token is sent as an authorization header."
          destination: GOOGLE_OWNED_SERVICE
        }
        policy {
          cookies_allowed: NO
          setting:
            "This feature can not be disabled by settings now"
          policy_exception_justification:
            "This feature is required to deliver core user experiences and "
            "cannot be disabled by policy."
        })");

  auto request = std::make_unique<network::ResourceRequest>();
  switch (type_) {
    case PerUserTopicSubscriptionRequest::RequestType::kSubscribe:
      request->method = "POST";
      break;
    case PerUserTopicSubscriptionRequest::RequestType::kUnsubscribe:
      request->method = "DELETE";
      break;
  }
  request->url = url;
  request->headers = headers;
  // Disable cookies for this request.
  request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  std::unique_ptr<network::SimpleURLLoader> url_loader =
      network::SimpleURLLoader::Create(std::move(request), traffic_annotation);
  url_loader->AttachStringForUpload(body, "application/json; charset=UTF-8");

  return url_loader;
}

}  // namespace invalidation
