// Copyright 2025 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/webapps/isolated_web_apps/url_loading/url_loader.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/byte_size.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/types/expected.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/web_package/web_bundle_utils.h"
#include "components/webapps/isolated_web_apps/reading/response_reader.h"
#include "components/webapps/isolated_web_apps/reading/response_reader_registry.h"
#include "components/webapps/isolated_web_apps/reading/response_reader_registry_factory.h"
#include "components/webapps/isolated_web_apps/url_loading/utils.h"
#include "content/public/browser/browser_context.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "net/base/net_errors.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/constants.h"
#include "services/network/public/cpp/loading_params.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_loader_completion_status.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace web_app {

namespace {

class IsolatedWebAppURLLoaderImpl : public network::mojom::URLLoader {
 public:
  IsolatedWebAppURLLoaderImpl(
      const base::FilePath& web_bundle_path,
      const web_package::SignedWebBundleId& web_bundle_id,
      mojo::PendingRemote<network::mojom::URLLoaderClient> loader_client,
      const network::ResourceRequest& resource_request,
      std::optional<content::FrameTreeNodeId> frame_tree_node_id)
      : loader_client_(std::move(loader_client)),
        resource_request_(resource_request),
        frame_tree_node_id_(frame_tree_node_id),
        web_bundle_path_(web_bundle_path),
        web_bundle_id_(web_bundle_id) {}

  IsolatedWebAppURLLoaderImpl(const IsolatedWebAppURLLoaderImpl&) = delete;
  IsolatedWebAppURLLoaderImpl& operator=(const IsolatedWebAppURLLoaderImpl&) =
      delete;
  ~IsolatedWebAppURLLoaderImpl() override = default;

  void Start(content::BrowserContext* browser_context) {
    IsolatedWebAppReaderRegistryFactory::Get(browser_context)
        ->ReadResponse(
            web_bundle_path_, web_bundle_id_, resource_request_,
            base::BindOnce(&IsolatedWebAppURLLoaderImpl::OnResponseRead,
                           weak_factory_.GetWeakPtr()));
  }

 private:
  void OnResponseRead(
      base::expected<IsolatedWebAppResponseReader::Response,
                     IsolatedWebAppReaderRegistry::ReadResponseError>
          response) {
    if (!loader_client_.is_connected()) {
      return;
    }

    if (!response.has_value()) {
      LogErrorMessageToConsole(frame_tree_node_id_, response.error().message);
      switch (response.error().type) {
        case IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError:
          loader_client_->OnComplete(
              network::URLLoaderCompletionStatus(net::ERR_INVALID_WEB_BUNDLE));
          return;
        case IsolatedWebAppReaderRegistry::ReadResponseError::Type::
            kResponseNotFound:
          // Return a synthetic 404 response.
          CompleteWithGeneratedResponse(std::move(loader_client_),
                                        net::HTTP_NOT_FOUND);
          return;
      }
    }

    // TODO(crbug.com/41474458): For the initial implementation, we allow only
    // net::HTTP_OK, but we should clarify acceptable status code in the spec.
    if (response->head()->response_code != net::HTTP_OK) {
      LogErrorMessageToConsole(
          frame_tree_node_id_,
          base::StringPrintf(
              "Failed to read response from Signed Web Bundle: The response "
              "has an unsupported HTTP status code: %d (only status code %d is "
              "allowed).",
              response->head()->response_code, net::HTTP_OK));
      loader_client_->OnComplete(
          network::URLLoaderCompletionStatus(net::ERR_INVALID_WEB_BUNDLE));
      return;
    }

    mojo::ScopedDataPipeProducerHandle producer_handle;
    mojo::ScopedDataPipeConsumerHandle consumer_handle;
    MojoCreateDataPipeOptions options;
    options.struct_size = sizeof(MojoCreateDataPipeOptions);
    options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE;
    options.element_num_bytes = 1;
    options.capacity_num_bytes =
        std::min(uint64_t{network::GetDataPipeDefaultAllocationSize()},
                 response->head()->payload_length);

    auto result =
        mojo::CreateDataPipe(&options, producer_handle, consumer_handle);
    if (result != MOJO_RESULT_OK) {
      loader_client_->OnComplete(
          network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
      return;
    }

    std::string header_string =
        web_package::CreateHeaderString(response->head());
    header_length_ = base::ByteSize(header_string.size());
    body_length_ = base::ByteSize(response->head()->payload_length);

    auto response_head =
        web_package::CreateResourceResponseFromHeaderString(header_string);
    // Copying `payload_length` into `body_length_` (a ByteSize) first ensures
    // it fits into `content_length` (an int64_t).
    response_head->content_length = body_length_.InBytes();
    loader_client_->OnReceiveResponse(std::move(response_head),
                                      std::move(consumer_handle), std::nullopt);

    response->ReadBody(
        std::move(producer_handle),
        base::BindOnce(&IsolatedWebAppURLLoaderImpl::FinishReadingBody,
                       weak_factory_.GetWeakPtr()));
  }

  void FinishReadingBody(net::Error net_error) {
    if (!loader_client_.is_connected()) {
      return;
    }

    network::URLLoaderCompletionStatus status(net_error);
    // For these values we use the same `body_length_` as we don't currently
    // provide encoding in Web Bundles.
    status.encoded_data_length = body_length_ + header_length_;
    status.encoded_body_length = body_length_;
    status.decoded_body_length = body_length_;
    loader_client_->OnComplete(status);
  }

  // network::mojom::URLLoader implementation
  void FollowRedirect(
      network::HttpRequestHeadersUpdateParams headers_update_params,
      const std::optional<GURL>& new_url) override {
    NOTREACHED();
  }
  void SetPriority(net::RequestPriority priority,
                   int intra_priority_value) override {}

  mojo::Remote<network::mojom::URLLoaderClient> loader_client_;
  base::ByteSize header_length_;
  base::ByteSize body_length_;
  const network::ResourceRequest resource_request_;
  std::optional<content::FrameTreeNodeId> frame_tree_node_id_;
  const base::FilePath web_bundle_path_;
  const web_package::SignedWebBundleId web_bundle_id_;

  base::WeakPtrFactory<IsolatedWebAppURLLoaderImpl> weak_factory_{this};
};

}  // namespace

// static
void IsolatedWebAppURLLoader::CreateAndStart(
    content::BrowserContext* browser_context,
    const base::FilePath& web_bundle_path,
    const web_package::SignedWebBundleId& web_bundle_id,
    mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderClient> loader_client,
    const network::ResourceRequest& resource_request,
    const std::optional<content::FrameTreeNodeId>& frame_tree_node_id) {
  // `browser_context` is not stored in the class to prevent having to deal with
  // dangling pointers (as this is a self-owned class without a shutdown
  // notifier).
  auto loader = std::make_unique<IsolatedWebAppURLLoaderImpl>(
      web_bundle_path, web_bundle_id, std::move(loader_client),
      resource_request, frame_tree_node_id);
  auto* raw_loader = loader.get();
  mojo::MakeSelfOwnedReceiver(std::move(loader), std::move(loader_receiver));
  raw_loader->Start(browser_context);
}

}  // namespace web_app
