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

#include "services/network/prefetch_url_loader_client.h"

#include <functional>
#include <optional>

#include "base/check.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "net/http/http_response_headers.h"
#include "services/network/prefetch_cache.h"
#include "services/network/prefetch_matches.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace network {

namespace {

// Returns true for a response code that we'd like to use.
bool IsNiceResponseCode(int response_code) {
  const int first_digit = response_code / 100;
  // 2xx response codes are "OK", "No content", etc.
  // 3xx response codes are "Moved permanently", "Not modified", etc.
  return first_digit == 2 || first_digit == 3;
}

// Returns true if this prefetch has attributes that make it unattractive to
// use.
bool ShouldAbandonPrefetch(const mojom::URLResponseHead& head) {
  CHECK(head.headers);
  const net::HttpResponseHeaders& headers = *head.headers;
  return !IsNiceResponseCode(headers.response_code()) ||
         headers.HasHeaderValue("vary", "purpose") ||
         headers.HasHeaderValue("vary", "sec-purpose") ||
         headers.HasHeaderValue("cache-control", "no-store");
}

}  // namespace

PrefetchURLLoaderClient::PrefetchURLLoaderClient(
    base::PassKey<PrefetchCache>,
    const net::NetworkIsolationKey& nik,
    const ResourceRequest& request,
    base::TimeTicks expiry_time,
    PrefetchCache* prefetch_cache)
    : request_(request),
      network_isolation_key_(nik),
      expiry_time_(expiry_time),
      prefetch_cache_(prefetch_cache) {
  CHECK(prefetch_cache_);
}

PrefetchURLLoaderClient::~PrefetchURLLoaderClient() = default;

mojo::PendingReceiver<mojom::URLLoader>
PrefetchURLLoaderClient::GetURLLoaderPendingReceiver() {
  CHECK(!url_loader_);
  auto pending_receiver = url_loader_.BindNewPipeAndPassReceiver();
  // This use of base::Unretained() is safe because the callback will not be
  // called after `url_loader_` is destroyed.
  url_loader_.set_disconnect_handler(base::BindOnce(
      &PrefetchURLLoaderClient::OnDisconnect, base::Unretained(this)));
  return pending_receiver;
}

mojo::PendingRemote<mojom::URLLoaderClient>
PrefetchURLLoaderClient::BindNewPipeAndPassRemote() {
  CHECK(!receiver_.is_bound());
  auto pending_remote = receiver_.BindNewPipeAndPassRemote();
  // This use of base::Unretained() is safe because the callback will not be
  // called after `receiver_` is destroyed.
  receiver_.set_disconnect_handler(base::BindOnce(
      &PrefetchURLLoaderClient::OnDisconnect, base::Unretained(this)));
  return pending_remote;
}

bool PrefetchURLLoaderClient::Matches(
    const ResourceRequest& real_request) const {
  return PrefetchMatches(request_, real_request);
}

void PrefetchURLLoaderClient::Consume(
    mojo::PendingReceiver<mojom::URLLoader> loader,
    mojo::PendingRemote<mojom::URLLoaderClient> client) {
  // Remove ourselves from the PrefetchCache.
  prefetch_cache_->Consume(this);

  // This is safe because we never call any methods on the URLLoader.
  mojo::PendingRemote<mojom::URLLoader> unbound_loader = url_loader_.Unbind();
  bool fuse_success =
      mojo::FusePipes(std::move(loader), std::move(unbound_loader));
  // TODO(ricea): Can this fail?
  CHECK(fuse_success);

  real_client_.Bind(std::move(client));
  // This use of base::Unretained is safe because the disconnect handler will
  // not be called after `real_client_` is destroyed.
  real_client_.set_disconnect_handler(base::BindOnce(
      &PrefetchURLLoaderClient::OnDisconnect, base::Unretained(this)));
  // Move the vector to a local just to be safe against re-entrancy.
  std::vector<base::OnceClosure> pending_callbacks =
      std::move(pending_callbacks_);
  // `pending_callbacks_` shouldn't be used again, but if it is, avoid undefined
  // behavior.
  pending_callbacks_.clear();
  for (auto&& callback : pending_callbacks) {
    std::move(callback).Run();
  }
  if (disconnected_) {
    OnDisconnect();
    // `this` is deleted here.
  }
}

void PrefetchURLLoaderClient::OnReceiveEarlyHints(
    mojom::EarlyHintsPtr early_hints) {
  ForwardOrRecord(&mojom::URLLoaderClient::OnReceiveEarlyHints,
                  std::move(early_hints));
}

void PrefetchURLLoaderClient::OnReceiveResponse(
    mojom::URLResponseHeadPtr head,
    mojo::ScopedDataPipeConsumerHandle body,
    std::optional<mojo_base::BigBuffer> cached_metadata) {
  // If a render process is already consuming this prefetch, it is too late to
  // decide we can't use it.
  if (!real_client_.is_bound() && ShouldAbandonPrefetch(*head)) {
    prefetch_cache_->Erase(this);
    return;
  }

  ForwardOrRecord(&mojom::URLLoaderClient::OnReceiveResponse, std::move(head),
                  std::move(body), std::move(cached_metadata));
}

void PrefetchURLLoaderClient::OnReceiveRedirect(
    const net::RedirectInfo& redirect_info,
    mojom::URLResponseHeadPtr head) {
  ForwardOrRecord(&mojom::URLLoaderClient::OnReceiveRedirect, redirect_info,
                  std::move(head));
}

void PrefetchURLLoaderClient::OnUploadProgress(
    int64_t current_position,
    int64_t total_size,
    OnUploadProgressCallback callback) {
  ForwardOrRecord(&mojom::URLLoaderClient::OnUploadProgress, current_position,
                  total_size, std::move(callback));
}

void PrefetchURLLoaderClient::OnTransferSizeUpdated(
    int32_t transfer_size_diff) {
  ForwardOrRecord(&mojom::URLLoaderClient::OnTransferSizeUpdated,
                  transfer_size_diff);
}

void PrefetchURLLoaderClient::OnComplete(
    const URLLoaderCompletionStatus& status) {
  ForwardOrRecord(&mojom::URLLoaderClient::OnComplete, status);
}

void PrefetchURLLoaderClient::OnDisconnect() {
  disconnected_ = true;
  if (real_client_.is_bound()) {
    // We don't need to queue the disconnect because all queued messages will
    // already have been sent to `real_client_`.
    // Just disconnect by deleting this object.
    prefetch_cache_->Erase(this);
  }
}

template <typename Method, typename... Args>
void PrefetchURLLoaderClient::ForwardOrRecord(Method method, Args... args) {
  if (real_client_.is_bound()) {
    ForwardToRealClient(method, std::forward<Args>(args)...);
  } else {
    // This use of `base::Unretained` is safe because the callback is owned by
    // this object.
    pending_callbacks_.push_back(base::BindOnce(
        &PrefetchURLLoaderClient::ForwardToRealClient<Method, Args...>,
        base::Unretained(this), method, std::forward<Args>(args)...));
  }
}

template <typename Method, typename... Args>
void PrefetchURLLoaderClient::ForwardToRealClient(Method method, Args... args) {
  std::invoke(method, real_client_, std::forward<Args>(args)...);
}

}  // namespace network
