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

#include "remoting/host/peer_connection_process.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/process/process.h"
#include "base/sequence_checker.h"
#include "base/task/single_thread_task_runner.h"
#include "ipc/ipc_channel_proxy.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/shared_remote.h"
#include "remoting/base/session_policies.h"
#include "remoting/host/crash_process.h"
#include "remoting/host/ipc_desktop_environment.h"
#include "remoting/host/peer_session_impl.h"
#include "remoting/protocol/ice_config_fetcher.h"
#include "remoting/protocol/transport.h"
#include "remoting/signaling/jingle_data_structures.h"

namespace remoting {

namespace {

class MojoIceConfigFetcher final : public protocol::IceConfigFetcher {
 public:
  explicit MojoIceConfigFetcher(
      mojo::SharedRemote<mojom::IceConfigFetcher> remote)
      : remote_(std::move(remote)) {}

  ~MojoIceConfigFetcher() override = default;

  void GetIceConfig(OnIceConfigCallback callback) override {
    if (!remote_) {
      std::move(callback).Run(std::nullopt);
      return;
    }
    remote_->GetIceConfig(base::BindOnce(
        [](OnIceConfigCallback callback,
           std::optional<protocol::IceConfig> ice_config) {
          std::move(callback).Run(std::move(ice_config));
        },
        std::move(callback)));
  }

 private:
  mojo::SharedRemote<mojom::IceConfigFetcher> remote_;
};

}  // namespace

PeerConnectionProcess::PeerConnectionProcess(
    scoped_refptr<base::SingleThreadTaskRunner> caller_task_runner,
    scoped_refptr<base::SingleThreadTaskRunner> io_task_runner)
    : caller_task_runner_(caller_task_runner),
      io_task_runner_(io_task_runner) {}

PeerConnectionProcess::~PeerConnectionProcess() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

bool PeerConnectionProcess::Start(
    mojo::ScopedMessagePipeHandle channel_handle) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!daemon_channel_);

  daemon_channel_ = IPC::ChannelProxy::Create(
      std::move(channel_handle), IPC::Channel::MODE_CLIENT, this,
      io_task_runner_, caller_task_runner_);

  return true;
}

void PeerConnectionProcess::CrashProcess(const std::string& function_name,
                                         const std::string& file_name,
                                         int line_number) {
  remoting::CrashProcess(function_name, file_name, line_number);
}

void PeerConnectionProcess::OnAssociatedInterfaceRequest(
    const std::string& interface_name,
    mojo::ScopedInterfaceEndpointHandle handle) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (interface_name == mojom::WorkerProcessControl::Name_) {
    if (worker_process_control_.is_bound()) {
      LOG(FATAL) << "Receiver already bound for associated interface: "
                 << mojom::WorkerProcessControl::Name_;
    }

    mojo::PendingAssociatedReceiver<mojom::WorkerProcessControl>
        pending_receiver(std::move(handle));
    worker_process_control_.Bind(std::move(pending_receiver));
  } else if (interface_name == mojom::PeerConnectionProcessControl::Name_) {
    if (control_receiver_.is_bound()) {
      LOG(FATAL) << "Receiver already bound for associated interface: "
                 << mojom::PeerConnectionProcessControl::Name_;
    }

    mojo::PendingAssociatedReceiver<mojom::PeerConnectionProcessControl>
        pending_receiver(std::move(handle));
    control_receiver_.Bind(std::move(pending_receiver));
    control_receiver_.set_disconnect_handler(base::BindOnce(
        &PeerConnectionProcess::OnChannelError, base::Unretained(this)));
  } else {
    LOG(FATAL) << "Received unexpected associated interface request: "
               << interface_name;
  }
}

void PeerConnectionProcess::BindPeerSession(
    mojo::PendingReceiver<mojom::PeerSession> session_receiver) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  session_receiver_.Bind(std::move(session_receiver));
  session_receiver_.set_disconnect_handler(base::BindOnce(
      &PeerConnectionProcess::OnSessionDisconnected, base::Unretained(this)));
}

void PeerConnectionProcess::Start(
    const std::string& client_jid,
    mojo::PendingRemote<mojom::PeerSessionEventHandler> event_handler,
    mojo::PendingRemote<mojom::DesktopSessionRequester>
        desktop_session_requester,
    mojo::PendingRemote<mojom::IceConfigFetcher> ice_config_fetcher,
    mojo::PendingRemote<mojom::PairingRequester> pairing_requester,
    const DesktopEnvironmentOptions& desktop_environment_options,
    const SessionPolicies& session_policies,
    const SessionOptions& session_options) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!peer_session_);

  desktop_session_requester_remote_.Bind(std::move(desktop_session_requester));
  desktop_session_requester_remote_.set_disconnect_handler(base::BindOnce(
      &PeerConnectionProcess::OnSessionDisconnected, base::Unretained(this)));

  desktop_environment_factory_ = std::make_unique<IpcDesktopEnvironmentFactory>(
      caller_task_runner_, io_task_runner_,
      base::BindRepeating(&PeerConnectionProcess::GetDesktopSession,
                          base::Unretained(this)));

  PeerSessionImplFactory::GetIceConfigFetcherCallback get_ice_config_fetcher_cb;
  if (ice_config_fetcher) {
    // Uses SharedRemote because `get_ice_config_fetcher_cb` is a repeating
    // callback that creates an IceConfigFetcher for each connection, but only a
    // single IceConfigFetcher pending remote is received from the Network
    // process over IPC. SharedRemote allows multiple IceConfigFetcher instances
    // to share the same underlying Mojo channel safely.
    mojo::SharedRemote<mojom::IceConfigFetcher> shared_remote(
        std::move(ice_config_fetcher), caller_task_runner_);
    get_ice_config_fetcher_cb = base::BindRepeating(
        [](mojo::SharedRemote<mojom::IceConfigFetcher> shared_remote)
            -> std::unique_ptr<protocol::IceConfigFetcher> {
          return std::make_unique<MojoIceConfigFetcher>(
              std::move(shared_remote));
        },
        std::move(shared_remote));
  } else {
    get_ice_config_fetcher_cb = base::BindRepeating(
        []() -> std::unique_ptr<protocol::IceConfigFetcher> {
          return nullptr;
        });
  }

  PeerSessionImplFactory::RequestPairingCallback request_pairing_cb;
  if (pairing_requester) {
    pairing_requester_remote_.Bind(std::move(pairing_requester));
    request_pairing_cb = base::BindRepeating(
        &PeerConnectionProcess::RequestPairing, base::Unretained(this));
  }

  PeerSessionImplFactory peer_session_factory(
      desktop_environment_factory_.get(), get_ice_config_fetcher_cb,
      std::move(request_pairing_cb));

  peer_session_ = peer_session_factory.Create();
  if (!peer_session_) {
    LOG(ERROR) << "Failed to create PeerSession.";
    Shutdown(1);
    return;
  }

  event_handler_.Bind(std::move(event_handler));
  peer_session_->Start(event_handler_.get(), client_jid,
                       desktop_environment_options, session_policies,
                       session_options);

  if (pending_start_transport_) {
    auto pending = std::move(*pending_start_transport_);
    pending_start_transport_.reset();
    StartTransport(pending.auth_key,
                   std::move(pending.transport_event_handler));
  }

  auto pending_transport_infos = std::move(pending_transport_infos_);
  for (const auto& transport_info : pending_transport_infos) {
    ProcessTransportInfo(transport_info);
  }
}

void PeerConnectionProcess::StartTransport(
    const std::string& auth_key,
    mojo::PendingRemote<mojom::TransportEventHandler> transport_event_handler) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!peer_session_ || !peer_session_->transport()) {
    pending_start_transport_ =
        PendingStartTransport{auth_key, std::move(transport_event_handler)};
    return;
  }
  transport_event_handler_.reset();
  if (transport_event_handler) {
    transport_event_handler_.Bind(std::move(transport_event_handler));
  }
  peer_session_->transport()->Start(
      auth_key, base::BindRepeating(&PeerConnectionProcess::OnSendTransportInfo,
                                    weak_factory_.GetWeakPtr()));
}

void PeerConnectionProcess::ProcessTransportInfo(
    const JingleTransportInfo& transport_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!peer_session_ || !peer_session_->transport()) {
    pending_transport_infos_.push_back(transport_info);
    return;
  }
  peer_session_->transport()->ProcessTransportInfo(transport_info);
}

void PeerConnectionProcess::OnSendTransportInfo(
    std::unique_ptr<JingleTransportInfo> transport_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!transport_info) {
    return;
  }
  if (transport_event_handler_.is_bound()) {
    transport_event_handler_->SendTransportInfo(std::move(*transport_info));
  }
}

void PeerConnectionProcess::DisconnectSession(
    protocol::ErrorCode error,
    const std::string& error_details,
    const SourceLocation& error_location) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (peer_session_) {
    peer_session_->DisconnectSession(error, error_details, error_location);
  }
}

void PeerConnectionProcess::OnChannelError() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  LOG(ERROR) << "Daemon channel error in PC process. Terminating.";
  Shutdown(1);
}

void PeerConnectionProcess::OnSessionDisconnected() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  LOG(INFO)
      << "PeerSession dropped by Network process; shutting down PC process.";
  desktop_session_requester_remote_.reset();
  pairing_requester_remote_.reset();
  transport_event_handler_.reset();
  pending_start_transport_.reset();
  pending_transport_infos_.clear();
  Shutdown(0);
}

void PeerConnectionProcess::RequestPairing(
    const std::string& client_name,
    PeerSessionFactory::RequestPairingResponseCallback response_cb) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!pairing_requester_remote_.is_bound()) {
    std::move(response_cb).Run(std::nullopt);
    return;
  }
  pairing_requester_remote_->RequestPairing(client_name,
                                            std::move(response_cb));
}

void PeerConnectionProcess::Shutdown(int exit_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (on_shutdown_for_testing_) {
    std::move(on_shutdown_for_testing_).Run();
    return;
  }
  base::Process::TerminateCurrentProcessImmediately(exit_code);
}

void PeerConnectionProcess::GetDesktopSession(
    mojo::PendingReceiver<mojom::DesktopSession> control_receiver,
    mojo::PendingRemote<mojom::DesktopSessionEvents> events_remote,
    mojom::DesktopSessionOptionsPtr options) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!desktop_session_requester_remote_.is_bound()) {
    LOG(ERROR)
        << "GetDesktopSession called when DesktopSessionRequester is not "
        << "bound.";
    return;
  }

  // The rest of `options` (such as `client_id` and `is_curtained`) is already
  // known by the network process and will be populated there.
  // TODO(crbug.com/552604892): Clean up the arguments of the GetDesktopSession
  // callback so it only takes the required parameters instead of full options.
  desktop_session_requester_remote_->RequestDesktopSession(
      std::move(control_receiver), std::move(events_remote),
      options ? options->screen_resolution : ScreenResolution());
}

}  // namespace remoting
