// 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 "net/socket/tcp_stream_attempt.h"

#include <memory>
#include <string_view>

#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "net/base/address_list.h"
#include "net/base/net_errors.h"
#include "net/socket/client_socket_factory.h"
#include "net/socket/socket_performance_watcher.h"
#include "net/socket/socket_performance_watcher_factory.h"
#include "net/socket/transport_client_socket.h"

namespace net {

// static
std::string_view TcpStreamAttempt::StateToString(State state) {
  switch (state) {
    case State::kNone:
      return "None";
    case State::kConnecting:
      return "Connecting";
  }
}

TcpStreamAttempt::TcpStreamAttempt(const StreamAttemptParams* params,
                                   IPEndPoint ip_endpoint,
                                   handles::NetworkHandle target_network,
                                   perfetto::Track track,
                                   const NetLogWithSource* net_log)
    : StreamAttempt(params,
                    ip_endpoint,
                    target_network,
                    track,
                    NetLogSourceType::TCP_STREAM_ATTEMPT,
                    NetLogEventType::TCP_STREAM_ATTEMPT_ALIVE,
                    net_log) {}

TcpStreamAttempt::~TcpStreamAttempt() {
  MaybeRecordConnectEnd(ERR_ABORTED);
}

LoadState TcpStreamAttempt::GetLoadState() const {
  switch (next_state_) {
    case State::kNone:
      return LOAD_STATE_IDLE;
    case State::kConnecting:
      return LOAD_STATE_CONNECTING;
  }
}

base::DictValue TcpStreamAttempt::GetInfoAsValue() const {
  base::DictValue dict;
  dict.Set("next_state", StateToString(next_state_));
  return dict;
}

int TcpStreamAttempt::StartInternal() {
  next_state_ = State::kConnecting;

  std::unique_ptr<SocketPerformanceWatcher> socket_performance_watcher;
  if (params().socket_performance_watcher_factory) {
    socket_performance_watcher =
        params()
            .socket_performance_watcher_factory->CreateSocketPerformanceWatcher(
                SocketPerformanceWatcherFactory::PROTOCOL_TCP,
                ip_endpoint().address());
  }

  std::unique_ptr<TransportClientSocket> stream_socket =
      params().client_socket_factory->CreateTransportClientSocket(
          AddressList(ip_endpoint()), target_network(),
          std::move(socket_performance_watcher),
          params().network_quality_estimator, net_log().net_log(),
          net_log().source());

  TransportClientSocket* socket_ptr = stream_socket.get();
  SetStreamSocket(std::move(stream_socket));

  mutable_connect_timing().connect_start = base::TimeTicks::Now();
  CHECK(!timeout_timer_.IsRunning());
  timeout_timer_.Start(
      FROM_HERE, kTcpHandshakeTimeout,
      base::BindOnce(&TcpStreamAttempt::OnTimeout, base::Unretained(this)));

  TRACE_EVENT_BEGIN("net.stream", "TcpConnect", track());
  net_log().AddEventReferencingSource(
      NetLogEventType::TCP_STREAM_ATTEMPT_CONNECT,
      socket_ptr->NetLog().source());
  int rv = socket_ptr->Connect(
      base::BindOnce(&TcpStreamAttempt::OnIOComplete, base::Unretained(this)));
  if (rv != ERR_IO_PENDING) {
    HandleCompletion(rv);
  }
  return rv;
}

base::DictValue TcpStreamAttempt::GetNetLogStartParams() {
  base::DictValue dict;
  dict.Set("ip_endpoint", ip_endpoint().ToString());
  return dict;
}

void TcpStreamAttempt::HandleCompletion(int rv) {
  MaybeRecordConnectEnd(rv);
  next_state_ = State::kNone;
  timeout_timer_.Stop();
  mutable_connect_timing().connect_end = base::TimeTicks::Now();
}

void TcpStreamAttempt::OnIOComplete(int rv) {
  CHECK_NE(rv, ERR_IO_PENDING);
  HandleCompletion(rv);
  NotifyOfCompletion(rv);
}

void TcpStreamAttempt::OnTimeout() {
  SetStreamSocket(nullptr);
  // TODO(bashi): The error code should be ERR_CONNECTION_TIMED_OUT but use
  // ERR_TIMED_OUT for consistency with ConnectJobs.
  OnIOComplete(ERR_TIMED_OUT);
}

void TcpStreamAttempt::MaybeRecordConnectEnd(int rv) {
  if (!timeout_timer_.IsRunning()) {
    return;
  }
  TRACE_EVENT_END("net.stream", track(), "result", rv);
}

}  // namespace net
