/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ASpdySession.h"  // because of SoftStreamError()
#include "Http3Session.h"
#include "Http3Stream.h"
#include "Http3StreamBase.h"
#include "Http3WebTransportSession.h"
#include "Http3ConnectUDPStream.h"
#include "Http3StreamTunnel.h"
#include "Http3WebTransportStream.h"
#include "HttpConnectionUDP.h"
#include "HttpLog.h"
#include "QuicSocketControl.h"
#include "SSLServerCertVerification.h"
#include "SSLTokensCache.h"
#include "ScopedNSSTypes.h"
#include "mozilla/RandomNum.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/glean/NetwerkDnsMetrics.h"
#include "mozilla/glean/NetwerkMetrics.h"
#include "mozilla/glean/NetwerkProtocolHttpMetrics.h"
#include "mozilla/net/DNS.h"
#include "nsHttpConnectionMgr.h"
#include "nsHttpHandler.h"
#include "nsHttpTransaction.h"
#include "nsIHttpActivityObserver.h"
#include "nsIOService.h"
#include "nsITLSSocketControl.h"
#include "nsNetAddr.h"
#include "nsQueryObject.h"
#include "nsSocketTransportService2.h"
#include "nsThreadUtils.h"
#include "sslerr.h"
#include "WebTransportCertificateVerifier.h"

namespace mozilla::net {

extern const nsCString& TRRProviderKey();

const uint64_t HTTP3_APP_ERROR_NO_ERROR = 0x100;
// const uint64_t HTTP3_APP_ERROR_GENERAL_PROTOCOL_ERROR = 0x101;
// const uint64_t HTTP3_APP_ERROR_INTERNAL_ERROR = 0x102;
// const uint64_t HTTP3_APP_ERROR_STREAM_CREATION_ERROR = 0x103;
// const uint64_t HTTP3_APP_ERROR_CLOSED_CRITICAL_STREAM = 0x104;
// const uint64_t HTTP3_APP_ERROR_FRAME_UNEXPECTED = 0x105;
// const uint64_t HTTP3_APP_ERROR_FRAME_ERROR = 0x106;
// const uint64_t HTTP3_APP_ERROR_EXCESSIVE_LOAD = 0x107;
// const uint64_t HTTP3_APP_ERROR_ID_ERROR = 0x108;
// const uint64_t HTTP3_APP_ERROR_SETTINGS_ERROR = 0x109;
// const uint64_t HTTP3_APP_ERROR_MISSING_SETTINGS = 0x10a;
const uint64_t HTTP3_APP_ERROR_REQUEST_REJECTED = 0x10b;
const uint64_t HTTP3_APP_ERROR_REQUEST_CANCELLED = 0x10c;
// const uint64_t HTTP3_APP_ERROR_REQUEST_INCOMPLETE = 0x10d;
// const uint64_t HTTP3_APP_ERROR_EARLY_RESPONSE = 0x10e;
// const uint64_t HTTP3_APP_ERROR_CONNECT_ERROR = 0x10f;
const uint64_t HTTP3_APP_ERROR_VERSION_FALLBACK = 0x110;

// const uint32_t UDP_MAX_PACKET_SIZE = 4096;
const uint32_t MAX_PTO_COUNTS = 16;

const uint32_t TRANSPORT_ERROR_STATELESS_RESET = 20;

NS_IMPL_ADDREF_INHERITED(Http3Session, nsAHttpConnection)
NS_IMPL_RELEASE_INHERITED(Http3Session, nsAHttpConnection)
NS_INTERFACE_MAP_BEGIN(Http3Session)
  NS_INTERFACE_MAP_ENTRY(nsAHttpConnection)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
  NS_INTERFACE_MAP_ENTRY_CONCRETE(Http3Session)
NS_INTERFACE_MAP_END

Http3Session::Http3Session() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG(("Http3Session::Http3Session [this=%p]", this));

  mCurrentBrowserId = gHttpHandler->ConnMgr()->CurrentBrowserId();
}

static nsresult RawBytesToNetAddr(uint16_t aFamily, const uint8_t* aRemoteAddr,
                                  uint16_t remotePort, NetAddr* netAddr) {
  if (aFamily == AF_INET) {
    netAddr->inet.family = AF_INET;
    netAddr->inet.port = htons(remotePort);
    memcpy(&netAddr->inet.ip, aRemoteAddr, 4);
  } else if (aFamily == AF_INET6) {
    netAddr->inet6.family = AF_INET6;
    netAddr->inet6.port = htons(remotePort);
    memcpy(&netAddr->inet6.ip.u8, aRemoteAddr, 16);
  } else {
    return NS_ERROR_UNEXPECTED;
  }

  return NS_OK;
}

nsresult Http3Session::Init(const nsHttpConnectionInfo* aConnInfo,
                            nsINetAddr* aSelfAddr, nsINetAddr* aPeerAddr,
                            HttpConnectionUDP* udpConn, uint32_t aProviderFlags,
                            nsIInterfaceRequestor* callbacks,
                            nsIUDPSocket* socket, bool aIsTunnel) {
  LOG3(("Http3Session::Init %p", this));

  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(udpConn);

  mConnInfo = aConnInfo->Clone();
  mNetAddr = aPeerAddr;

  // When `isOuterConnection` is true, this Http3Session represents the *outer*
  // connection between Firefox and the proxy (e.g., when using CONNECT-UDP).
  //
  // We track this flag for two main reasons:
  // 1. To select the correct hostname during TLS negotiation on the outer
  //    connection.
  // 2. To explicitly enable Path MTU Discovery (PMTUD) on the outer connection,
  //    since the outer path’s MTU must be at least as large as the inner one.
  bool isOuterConnection = false;
  if (!aIsTunnel) {
    if (auto* proxyInfo = aConnInfo->ProxyInfo()) {
      isOuterConnection = proxyInfo->IsHttp3Proxy();
    }
  }

  // Create security control and info object for quic.
  mSocketControl = new QuicSocketControl(
      isOuterConnection ? aConnInfo->ProxyInfo()->Host()
                        : aConnInfo->GetOrigin(),
      isOuterConnection ? aConnInfo->ProxyInfo()->Port()
                        : aConnInfo->OriginPort(),
      aProviderFlags, aConnInfo->GetOriginAttributes(), this);
  const nsCString& alpn = isOuterConnection ? aConnInfo->GetProxyNPNToken()
                                            : aConnInfo->GetNPNToken();

  NetAddr selfAddr;
  MOZ_ALWAYS_SUCCEEDS(aSelfAddr->GetNetAddr(&selfAddr));
  NetAddr peerAddr;
  MOZ_ALWAYS_SUCCEEDS(aPeerAddr->GetNetAddr(&peerAddr));

  LOG3(
      ("Http3Session::Init origin=%s, alpn=%s, selfAddr=%s, peerAddr=%s,"
       " qpack table size=%u, max blocked streams=%u webtransport=%d "
       "[this=%p]",
       PromiseFlatCString(mSocketControl->GetHostName()).get(),
       PromiseFlatCString(alpn).get(), selfAddr.ToString().get(),
       peerAddr.ToString().get(), gHttpHandler->DefaultQpackTableSize(),
       gHttpHandler->DefaultHttp3MaxBlockedStreams(),
       mConnInfo->GetWebTransport(), this));

  if (mConnInfo->GetWebTransport()) {
    ExtState(ExtendedConnectKind::WebTransport).mStatus = NEGOTIATING;
  }
  if (isOuterConnection) {
    ExtState(ExtendedConnectKind::ConnectUDP).mStatus = NEGOTIATING;
  }

  mUseNSPRForIO =
      StaticPrefs::network_http_http3_use_nspr_for_io() || aIsTunnel;

  uint32_t idleTimeout =
      mConnInfo->GetIsTrrServiceChannel()
          ? StaticPrefs::network_trr_idle_timeout_for_http3_conn()
          : StaticPrefs::network_http_http3_idle_timeout();

  // 0 means "use neqo's spec-compliant default PTO scaling".
  uint32_t fastPto = mConnInfo->GetIsTrrServiceChannel()
                         ? StaticPrefs::network_trr_fast_pto_for_http3_conn()
                         : 0;

  nsresult rv;
  if (mUseNSPRForIO) {
    rv = NeqoHttp3Conn::InitUseNSPRForIO(
        mSocketControl->GetHostName(), alpn, selfAddr, peerAddr,
        gHttpHandler->DefaultQpackTableSize(),
        gHttpHandler->DefaultHttp3MaxBlockedStreams(),
        StaticPrefs::network_http_http3_max_data(),
        StaticPrefs::network_http_http3_max_stream_data(),
        StaticPrefs::network_http_http3_version_negotiation_enabled(),
        mConnInfo->GetWebTransport(), gHttpHandler->Http3QlogDir(), idleTimeout,
        fastPto, getter_AddRefs(mHttp3Connection));
  } else {
    rv = NeqoHttp3Conn::Init(
        mSocketControl->GetHostName(), alpn, selfAddr, peerAddr,
        gHttpHandler->DefaultQpackTableSize(),
        gHttpHandler->DefaultHttp3MaxBlockedStreams(),
        StaticPrefs::network_http_http3_max_data(),
        StaticPrefs::network_http_http3_max_stream_data(),
        StaticPrefs::network_http_http3_version_negotiation_enabled(),
        mConnInfo->GetWebTransport(), gHttpHandler->Http3QlogDir(), idleTimeout,
        fastPto, socket->GetFileDescriptor(), isOuterConnection,
        getter_AddRefs(mHttp3Connection));
  }
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsAutoCString peerId;
  mSocketControl->GetPeerId(peerId);
  nsTArray<uint8_t> token;
  SessionCacheInfo info;
  udpConn->ChangeConnectionState(ConnectionState::TLS_HANDSHAKING);

  auto hasServCertHashes = [&]() -> bool {
    if (!mConnInfo->GetWebTransport()) {
      return false;
    }
    const nsTArray<RefPtr<nsIWebTransportHash>>* servCertHashes =
        gHttpHandler->ConnMgr()->GetServerCertHashes(mConnInfo);
    return servCertHashes && !servCertHashes->IsEmpty();
  };

  // See https://github.com/mozilla/neqo/issues/2442.
  // We need to set ECH first before set resumption token.
  auto config = mConnInfo->GetEchConfig();
  if (config.IsEmpty()) {
    if (StaticPrefs::security_tls_ech_grease_http3() && config.IsEmpty()) {
      if ((RandomUint64().valueOr(0) % 100) >=
          100 - StaticPrefs::security_tls_ech_grease_probability()) {
        // Setting an empty config enables GREASE mode.
        mSocketControl->SetEchConfig(config);
        mEchExtensionStatus = EchExtensionStatus::kGREASE;
      }
    }
  } else if (nsHttpHandler::EchConfigEnabled(true) && !config.IsEmpty()) {
    mSocketControl->SetEchConfig(config);
    mEchExtensionStatus = EchExtensionStatus::kReal;
    HttpConnectionActivity activity(
        mConnInfo->HashKey(), mConnInfo->GetOrigin(), mConnInfo->OriginPort(),
        mConnInfo->EndToEndSSL(), !mConnInfo->GetEchConfig().IsEmpty(),
        mConnInfo->IsHttp3());
    gHttpHandler->ObserveHttpActivityWithArgs(
        activity, NS_ACTIVITY_TYPE_HTTP_CONNECTION,
        NS_HTTP_ACTIVITY_SUBTYPE_ECH_SET, PR_Now(), 0, ""_ns);
  } else {
    mEchExtensionStatus = EchExtensionStatus::kNotPresent;
  }

  // In WebTransport, when servCertHashes is specified, it indicates that the
  // connection to the WebTransport server should authenticate using the
  // expected certificate hash. Therefore, 0RTT should be disabled in this
  // context to ensure the certificate hash is checked.
  if (StaticPrefs::network_http_http3_enable_0rtt() && !hasServCertHashes()) {
    uint32_t maxAttempts =
        StaticPrefs::network_ssl_tokens_cache_records_per_entry();
    for (uint32_t attempt = 0; attempt < maxAttempts; ++attempt) {
      if (NS_FAILED(SSLTokensCache::Get(peerId, token, info))) {
        break;
      }
      LOG(("Found a resumption token in the cache [attempt=%u].", attempt));
      nsresult rv = mHttp3Connection->SetResumptionToken(token);
      if (NS_FAILED(rv)) {
        LOG(("SetResumptionToken failed [attempt=%u], trying next token",
             attempt));
        continue;
      }
      mSocketControl->SetSessionCacheInfo(std::move(info));
      if (mHttp3Connection->IsZeroRtt()) {
        LOG(("Can send ZeroRtt data"));
        RefPtr<Http3Session> self(this);
        mState = ZERORTT;
        udpConn->ChangeConnectionState(ConnectionState::ZERORTT);
        mZeroRttStarted = TimeStamp::Now();
        // Let the nsHttpConnectionMgr know that the connection can accept
        // transactions.
        // We need to dispatch the following function to this thread so that
        // it is executed after the current function. At this point a
        // Http3Session is still being initialized and ReportHttp3Connection
        // will try to dispatch transaction on this session therefore it
        // needs to be executed after the initializationg is done.
        nsCOMPtr<nsIRunnable> event =
            NS_NewRunnableFunction("Http3Session::ReportHttp3Connection",
                                   [self]() { self->ReportHttp3Connection(); });
        if (StaticPrefs::network_trr_high_priority_events() &&
            mConnInfo->GetIsTrrServiceChannel()) {
          event = new PrioritizableRunnable(
              event.forget(), nsIRunnablePriority::PRIORITY_MEDIUMHIGH);
        }
        DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(event);
        NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                             "NS_DispatchToCurrentThread failed");
      }
      break;
    }
  }

  if (mState != ZERORTT) {
    ZeroRttTelemetry(ZeroRttOutcome::NOT_USED);
  }

  // After this line, Http3Session and HttpConnectionUDP become a cycle. We put
  // this line in the end of Http3Session::Init to make sure Http3Session can be
  // released when Http3Session::Init early returned.
  mUdpConn = udpConn;
  return NS_OK;
}

void Http3Session::DoSetEchConfig(const nsACString& aEchConfig) {
  LOG(("Http3Session::DoSetEchConfig %p of length %zu", this,
       aEchConfig.Length()));
  nsTArray<uint8_t> config;
  config.AppendElements(
      reinterpret_cast<const uint8_t*>(aEchConfig.BeginReading()),
      aEchConfig.Length());
  mHttp3Connection->SetEchConfig(config);
}

nsresult Http3Session::SendPriorityUpdateFrame(uint64_t aStreamId,
                                               uint8_t aPriorityUrgency,
                                               bool aPriorityIncremental) {
  return mHttp3Connection->PriorityUpdate(aStreamId, aPriorityUrgency,
                                          aPriorityIncremental);
}

// Shutdown the http3session and close all transactions.
void Http3Session::Shutdown() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  if (mTimer) {
    mTimer->Cancel();
  }
  mTimer = nullptr;
  mTimerCallback = nullptr;

  bool isEchRetry = mError == mozilla::psm::GetXPCOMFromNSSError(
                                  SSL_ERROR_ECH_RETRY_WITH_ECH);
  bool isNSSError = psm::IsNSSErrorCode(-1 * NS_ERROR_GET_CODE(mError));
  bool allowToRetryWithDifferentIPFamily =
      mBeforeConnectedError &&
      gHttpHandler->ConnMgr()->AllowToRetryDifferentIPFamilyForHttp3(mConnInfo,
                                                                     mError);
  LOG(("Http3Session::Shutdown %p allowToRetryWithDifferentIPFamily=%d", this,
       allowToRetryWithDifferentIPFamily));
  // Don't exclude H3 if the failure was in the 0-RTT phase: the PSK ticket
  // is single-use, so the retry will do a full TLS handshake and the H3
  // server itself should still be reachable.
  if (mBeforeConnectedError && mHad0RttStream) {
    mDontExclude = true;
  }
  if ((mBeforeConnectedError ||
       (mError == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR)) &&
      !isNSSError && !isEchRetry && !mConnInfo->GetWebTransport() &&
      !allowToRetryWithDifferentIPFamily && !mDontExclude) {
    gHttpHandler->ExcludeHttp3(mConnInfo);
    if (mFirstHttpTransaction) {
      mFirstHttpTransaction->DisableHttp3(false);
    }
  }

  nsTArray<RefPtr<Http3StreamBase>> streams;
  streams.SetCapacity(mStreamTransactionHash.Count());
  for (const auto& s : mStreamTransactionHash.Values()) {
    streams.AppendElement(s);
  }
  for (const auto& stream : streams) {
    if (mBeforeConnectedError) {
      // We have an error before we were connected, just restart transactions.
      // The transaction restart code path will remove AltSvc mapping and the
      // direct path will be used.
      MOZ_ASSERT(NS_FAILED(mError));
      if (isEchRetry) {
        // We have to propagate this error to nsHttpTransaction, so the
        // transaction will be restarted with a new echConfig.
        stream->Close(mError);
      } else if (isNSSError) {
        stream->Close(mError);
      } else {
        if (allowToRetryWithDifferentIPFamily && mNetAddr) {
          NetAddr addr;
          mNetAddr->GetNetAddr(&addr);
          gHttpHandler->ConnMgr()->SetRetryDifferentIPFamilyForHttp3(
              mConnInfo, addr.raw.family);
          // We want the transaction to be restarted with the same connection
          // info.
          stream->Transaction()->DoNotRemoveAltSvc();
          // We already set the preference in SetRetryDifferentIPFamilyForHttp3,
          // so we want to keep it for the next retry.
          stream->Transaction()->DoNotResetIPFamilyPreference();
          stream->Close(NS_ERROR_NET_RESET);
          // Since Http3Session::Shutdown can be called multiple times, we set
          // mDontExclude for not putting this domain into the excluded list.
          mDontExclude = true;
        } else {
          stream->Close(NS_ERROR_NET_RESET);
        }
      }
    } else if (!stream->HasStreamId()) {
      if (NS_SUCCEEDED(mError)) {
        // Connection has not been started yet. We can restart it.
        stream->Transaction()->DoNotRemoveAltSvc();
      }
      stream->Close(NS_ERROR_NET_RESET);
    } else if (stream->GetHttp3Stream() &&
               stream->GetHttp3Stream()->RecvdData()) {
      stream->Close(NS_ERROR_NET_PARTIAL_TRANSFER);
    } else if (mError == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR) {
      stream->Close(NS_ERROR_NET_HTTP3_PROTOCOL_ERROR);
    } else if (mError == NS_ERROR_NET_RESET) {
      stream->Close(NS_ERROR_NET_RESET);
    } else {
      stream->Close(NS_ERROR_ABORT);
    }
    RemoveStreamFromQueues(stream);
    if (stream->HasStreamId()) {
      mStreamIdHash.Remove(stream->StreamId());
    }
  }
  mStreamTransactionHash.Clear();

  for (const auto& stream : mWebTransportSessions) {
    stream->Close(NS_ERROR_ABORT);
    RemoveStreamFromQueues(stream);
    mStreamIdHash.Remove(stream->StreamId());
  }
  mWebTransportSessions.Clear();

  for (const auto& stream : mTunnelStreams) {
    stream->Close(NS_ERROR_ABORT);
    RemoveStreamFromQueues(stream);
    mStreamIdHash.Remove(stream->StreamId());
  }
  mTunnelStreams.Clear();

  for (const auto& stream : mWebTransportStreams) {
    stream->Close(NS_ERROR_ABORT);
    RemoveStreamFromQueues(stream);
    mStreamIdHash.Remove(stream->StreamId());
  }

  RefPtr<Http3StreamBase> stream;
  while ((stream = mQueuedStreams.PopFront())) {
    LOG(("Close remaining stream in queue:%p", stream.get()));
    stream->SetQueued(false);
    stream->Close(NS_ERROR_ABORT);
  }
  mWebTransportStreams.Clear();
}

Http3Session::~Http3Session() {
  LOG3(("Http3Session::~Http3Session %p", this));
#ifndef ANDROID
  EchOutcomeTelemetry();
#endif
  glean::http3::request_per_conn.AccumulateSingleSample(mTransactionCount);
  glean::http3::blocked_by_stream_limit_per_conn.AccumulateSingleSample(
      mBlockedByStreamLimitCount);
  glean::http3::trans_blocked_by_stream_limit_per_conn.AccumulateSingleSample(
      mTransactionsBlockedByStreamLimitCount);

  glean::http3::trans_sending_blocked_by_flow_control_per_conn
      .AccumulateSingleSample(mTransactionsSenderBlockedByFlowControlCount);

  if (mTrrStreams) {
    mozilla::glean::networking::trr_request_count_per_conn
        .Get(nsPrintfCString("%s_h3", mConnInfo->Origin()))
        .Add(static_cast<int32_t>(mTrrStreams));
  }

  Shutdown();
}

// This function may return a socket error.
// It will not return an error if socket error is
// NS_BASE_STREAM_WOULD_BLOCK.
// A caller of this function will close the Http3 connection
// in case of an error.
// The only callers is Http3Session::RecvData.
nsresult Http3Session::ProcessInput(nsIUDPSocket* socket) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(mUdpConn);

  LOG(("Http3Session::ProcessInput writer=%p [this=%p state=%d]",
       mUdpConn.get(), this, mState));

  if (!socket || socket->IsSocketClosed()) {
    MOZ_DIAGNOSTIC_ASSERT(false, "UDP socket should still be open");
    return NS_ERROR_UNEXPECTED;
  }

  if (mUseNSPRForIO) {
    while (true) {
      nsTArray<uint8_t> data;
      NetAddr addr{};
      // RecvWithAddr actually does not return an error.
      nsresult rv = socket->RecvWithAddr(&addr, data);
      MOZ_ALWAYS_SUCCEEDS(rv);
      if (NS_FAILED(rv) || data.IsEmpty()) {
        break;
      }
      rv = mHttp3Connection->ProcessInputUseNSPRForIO(addr, data);
      MOZ_ALWAYS_SUCCEEDS(rv);
      if (NS_FAILED(rv)) {
        break;
      }

      LOG(("Http3Session::ProcessInput received=%zu", data.Length()));
      mTotalBytesRead += static_cast<int64_t>(data.Length());
    }

    return NS_OK;
  }

  // Not using NSPR.

  auto rv = mHttp3Connection->ProcessInput();
  // Note: WOULD_BLOCK is handled in neqo_glue.
  if (NS_FAILED(rv.result)) {
    mSocketError = rv.result;
    // If there was an error return from here. We do not need to set a timer,
    // because we will close the connection.
    return rv.result;
  }
  mTotalBytesRead += rv.bytes_read;
  socket->AddInputBytes(rv.bytes_read);

  return NS_OK;
}

nsresult Http3Session::ProcessTransactionRead(uint64_t stream_id) {
  RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(stream_id);
  if (!stream) {
    LOG(
        ("Http3Session::ProcessTransactionRead - stream not found "
         "stream_id=0x%" PRIx64 " [this=%p].",
         stream_id, this));
    return NS_OK;
  }

  return ProcessTransactionRead(stream);
}

nsresult Http3Session::ProcessTransactionRead(Http3StreamBase* stream) {
  nsresult rv = stream->WriteSegments();

  if (ASpdySession::SoftStreamError(rv) || stream->Done()) {
    LOG3(
        ("Http3Session::ProcessSingleTransactionRead session=%p stream=%p "
         "0x%" PRIx64 " cleanup stream rv=0x%" PRIx32 " done=%d.\n",
         this, stream, stream->StreamId(), static_cast<uint32_t>(rv),
         stream->Done()));
    CloseStream(stream,
                (rv == NS_BINDING_RETARGETED) ? NS_BINDING_RETARGETED : NS_OK);
    return NS_OK;
  }

  if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
    return rv;
  }
  return NS_OK;
}

nsresult Http3Session::ProcessEvents() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  RefPtr<Http3Session> self(this);

  LOG(("Http3Session::ProcessEvents [this=%p]", this));

  // We need an array to pick up header data or a resumption token.
  nsTArray<uint8_t> data;
  Http3Event event{};
  event.tag = Http3Event::Tag::NoEvent;

  nsresult rv = mHttp3Connection->GetEvent(&event, data);
  if (NS_FAILED(rv)) {
    LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this,
         static_cast<uint32_t>(rv)));
    return rv;
  }

  while (event.tag != Http3Event::Tag::NoEvent) {
    switch (event.tag) {
      case Http3Event::Tag::HeaderReady: {
        MOZ_ASSERT(mState == CONNECTED);
        LOG(("Http3Session::ProcessEvents - HeaderReady"));
        uint64_t id = event.header_ready.stream_id;

        RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(id);
        if (!stream) {
          LOG(
              ("Http3Session::ProcessEvents - HeaderReady - stream not found "
               "stream_id=0x%" PRIx64 " [this=%p].",
               id, this));
          break;
        }

        MOZ_RELEASE_ASSERT(stream->GetHttp3Stream(),
                           "This must be a Http3Stream");

        stream->SetResponseHeaders(data, event.header_ready.fin,
                                   event.header_ready.interim);

        RefPtr<Http3Stream> http3Stream = stream->GetHttp3Stream();
        MOZ_RELEASE_ASSERT(http3Stream, "This must be a Http3Stream");
        RefPtr<nsAHttpTransaction> trans = http3Stream->Transaction();
        nsHttpTransaction* httpTrans =
            trans ? trans->QueryHttpTransaction() : nullptr;
        if (httpTrans) {
          if (event.header_ready.interim) {
            if (httpTrans->GetFirstInterimResponseStart().IsNull()) {
              auto now = TimeStamp::Now();
              httpTrans->SetFirstInterimResponseStart(now, true);
              httpTrans->SetResponseStart(now, false);
            }
          } else {
            auto now = TimeStamp::Now();
            httpTrans->SetFinalResponseHeadersStart(now, true);
            TimeStamp firstInterim = httpTrans->GetFirstInterimResponseStart();
            if (!firstInterim.IsNull()) {
              httpTrans->SetResponseStart(firstInterim, false);
            } else {
              httpTrans->SetResponseStart(now, false);
            }
          }
        }

        rv = ProcessTransactionRead(stream);

        if (NS_FAILED(rv)) {
          LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this,
               static_cast<uint32_t>(rv)));
          return rv;
        }

        mUdpConn->NotifyDataRead();
        break;
      }
      case Http3Event::Tag::DataReadable: {
        MOZ_ASSERT(mState == CONNECTED);
        LOG(("Http3Session::ProcessEvents - DataReadable"));
        uint64_t id = event.data_readable.stream_id;

        nsresult rv = ProcessTransactionRead(id);

        if (NS_FAILED(rv)) {
          LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this,
               static_cast<uint32_t>(rv)));
          return rv;
        }
        break;
      }
      case Http3Event::Tag::DataWritable: {
        MOZ_ASSERT(CanSendData());
        LOG(("Http3Session::ProcessEvents - DataWritable"));

        RefPtr<Http3StreamBase> stream =
            mStreamIdHash.Get(event.data_writable.stream_id);

        if (stream) {
          StreamReadyToWrite(stream);
          stream->SetBlockedByFlowControl(false);
        }
      } break;
      case Http3Event::Tag::Reset:
        LOG(("Http3Session::ProcessEvents %p - Reset", this));
        ResetOrStopSendingRecvd(event.reset.stream_id, event.reset.error,
                                RESET);
        break;
      case Http3Event::Tag::StopSending:
        LOG(
            ("Http3Session::ProcessEvents %p - StopSeniding with error "
             "0x%" PRIx64,
             this, event.stop_sending.error));
        if (event.stop_sending.error == HTTP3_APP_ERROR_NO_ERROR) {
          RefPtr<Http3StreamBase> stream =
              mStreamIdHash.Get(event.stop_sending.stream_id);
          if (stream) {
            if (RefPtr<Http3Stream> httpStream = stream->GetHttp3Stream()) {
              httpStream->StopSending();
            } else {
              ResetOrStopSendingRecvd(event.stop_sending.stream_id,
                                      event.stop_sending.error, STOP_SENDING);
            }
          }
        } else {
          ResetOrStopSendingRecvd(event.stop_sending.stream_id,
                                  event.stop_sending.error, STOP_SENDING);
        }
        break;
      case Http3Event::Tag::PushPromise:
        LOG(("Http3Session::ProcessEvents - PushPromise"));
        break;
      case Http3Event::Tag::PushHeaderReady:
        LOG(("Http3Session::ProcessEvents - PushHeaderReady"));
        break;
      case Http3Event::Tag::PushDataReadable:
        LOG(("Http3Session::ProcessEvents - PushDataReadable"));
        break;
      case Http3Event::Tag::PushCanceled:
        LOG(("Http3Session::ProcessEvents - PushCanceled"));
        break;
      case Http3Event::Tag::RequestsCreatable:
        LOG(("Http3Session::ProcessEvents - StreamCreatable"));
        ProcessPending();
        break;
      case Http3Event::Tag::AuthenticationNeeded:
        LOG(("Http3Session::ProcessEvents - AuthenticationNeeded %d",
             mAuthenticationStarted));
        if (!mAuthenticationStarted) {
          mAuthenticationStarted = true;
          LOG(("Http3Session::ProcessEvents - AuthenticationNeeded called"));
          CallCertVerification(Nothing());
        }
        break;
      case Http3Event::Tag::ZeroRttRejected:
        LOG(("Http3Session::ProcessEvents - ZeroRttRejected"));
        if (mState == ZERORTT) {
          mState = INITIALIZING;
          mTransactionCount = 0;
          Finish0Rtt(true);
          if (IsClosing()) {
            break;
          }
          ZeroRttTelemetry(ZeroRttOutcome::USED_REJECTED);
        }
        break;
      case Http3Event::Tag::ResumptionToken: {
        LOG(("Http3Session::ProcessEvents - ResumptionToken"));
        if (StaticPrefs::network_http_http3_enable_0rtt() && !data.IsEmpty()) {
          LOG(("Got a resumption token"));
          nsAutoCString peerId;
          mSocketControl->GetPeerId(peerId);
          if (NS_FAILED(SSLTokensCache::Put(
                  peerId, data.Elements(), data.Length(), mSocketControl,
                  PR_Now() + event.resumption_token.expire_in))) {
            LOG(("Adding resumption token failed"));
          }
        }
      } break;
      case Http3Event::Tag::ConnectionConnected: {
        LOG(("Http3Session::ProcessEvents - ConnectionConnected"));
        if (IsClosing()) {
          break;
        }
        bool was0RTT = mState == ZERORTT;
        mState = CONNECTED;
        SetSecInfo();
        mSocketControl->HandshakeCompleted();
        if (was0RTT) {
          Finish0Rtt(false);
          if (IsClosing()) {
            break;
          }
          ZeroRttTelemetry(ZeroRttOutcome::USED_SUCCEEDED);
        }

        OnTransportStatus(nullptr, NS_NET_STATUS_CONNECTED_TO, 0);
        mUdpConn->OnConnected();
        ReportHttp3Connection();
        // Maybe call ResumeSend:
        // In case ZeroRtt has been used and it has been rejected, 2 events will
        // be received: ZeroRttRejected and ConnectionConnected. ZeroRttRejected
        // that will put all transaction into mReadyForWrite queue and it will
        // call MaybeResumeSend, but that will not have an effect because the
        // connection is ont in CONNECTED state. When ConnectionConnected event
        // is received call MaybeResumeSend to trigger reads for the
        // zero-rtt-rejected transactions.
        MaybeResumeSend();
      } break;
      case Http3Event::Tag::GoawayReceived:
        LOG(("Http3Session::ProcessEvents - GoawayReceived"));
        mUdpConn->SetCloseReason(ConnectionCloseReason::GO_AWAY);
        mGoawayReceived = true;
        break;
      case Http3Event::Tag::ConnectionClosing:
        LOG(("Http3Session::ProcessEvents - ConnectionClosing"));
        if (NS_SUCCEEDED(mError) && !IsClosing()) {
          mError = NS_ERROR_NET_HTTP3_PROTOCOL_ERROR;
          CloseConnectionTelemetry(event.connection_closing.error, true);

          auto isStatelessResetOrNoError = [](CloseError& aError) -> bool {
            if (aError.tag == CloseError::Tag::TransportInternalErrorOther &&
                aError.transport_internal_error_other._0 ==
                    TRANSPORT_ERROR_STATELESS_RESET) {
              return true;
            }
            if (aError.tag == CloseError::Tag::TransportError &&
                aError.transport_error._0 == 0) {
              return true;
            }
            if (aError.tag == CloseError::Tag::PeerError &&
                aError.peer_error._0 == 0) {
              return true;
            }
            if (aError.tag == CloseError::Tag::AppError &&
                aError.app_error._0 == HTTP3_APP_ERROR_NO_ERROR) {
              return true;
            }
            if (aError.tag == CloseError::Tag::PeerAppError &&
                aError.peer_app_error._0 == HTTP3_APP_ERROR_NO_ERROR) {
              return true;
            }
            return false;
          };
          if (isStatelessResetOrNoError(event.connection_closing.error)) {
            mError = NS_ERROR_NET_RESET;
          }
          if (event.connection_closing.error.tag == CloseError::Tag::EchRetry) {
            mSocketControl->SetRetryEchConfig(Substring(
                reinterpret_cast<const char*>(data.Elements()), data.Length()));
            mError = psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH);
          }
        }
        return mError;
        break;
      case Http3Event::Tag::ConnectionClosed:
        LOG(("Http3Session::ProcessEvents - ConnectionClosed"));
        if (NS_SUCCEEDED(mError)) {
          mError = NS_ERROR_NET_TIMEOUT;
          mUdpConn->SetCloseReason(ConnectionCloseReason::IDLE_TIMEOUT);
          CloseConnectionTelemetry(event.connection_closed.error, false);
        }
        mIsClosedByNeqo = true;
        if (event.connection_closed.error.tag == CloseError::Tag::EchRetry) {
          mSocketControl->SetRetryEchConfig(Substring(
              reinterpret_cast<const char*>(data.Elements()), data.Length()));
          mError = psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH);
        }
        LOG(("Http3Session::ProcessEvents - ConnectionClosed error=%" PRIx32,
             static_cast<uint32_t>(mError)));
        // We need to return here and let HttpConnectionUDP close the session.
        return mError;
        break;
      case Http3Event::Tag::EchFallbackAuthenticationNeeded: {
        nsCString echPublicName(reinterpret_cast<const char*>(data.Elements()),
                                data.Length());
        LOG(
            ("Http3Session::ProcessEvents - EchFallbackAuthenticationNeeded "
             "echPublicName=%s",
             echPublicName.get()));
        if (!mAuthenticationStarted) {
          mAuthenticationStarted = true;
          CallCertVerification(Some(echPublicName));
        }
      } break;
      case Http3Event::Tag::WebTransport: {
        switch (event.web_transport._0.tag) {
          case WebTransportEventExternal::Tag::Negotiated:
            LOG(("Http3Session::ProcessEvents - WebTransport %d",
                 event.web_transport._0.negotiated._0));
            FinishNegotiation(ExtendedConnectKind::WebTransport,
                              event.web_transport._0.negotiated._0);
            break;
          case WebTransportEventExternal::Tag::Session: {
            MOZ_ASSERT(mState == CONNECTED);

            uint64_t id = event.web_transport._0.session._0;
            LOG(
                ("Http3Session::ProcessEvents - WebTransport Session "
                 " sessionId=0x%" PRIx64,
                 id));
            RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(id);
            if (!stream) {
              LOG(
                  ("Http3Session::ProcessEvents - WebTransport Session - "
                   "stream not found "
                   "stream_id=0x%" PRIx64 " [this=%p].",
                   id, this));
              break;
            }

            MOZ_RELEASE_ASSERT(stream->GetHttp3WebTransportSession(),
                               "It must be a WebTransport session");
            stream->SetResponseHeaders(data, false, false);

            rv = stream->WriteSegments();

            if (ASpdySession::SoftStreamError(rv) || stream->Done()) {
              LOG3(
                  ("Http3Session::ProcessSingleTransactionRead session=%p "
                   "stream=%p "
                   "0x%" PRIx64 " cleanup stream rv=0x%" PRIx32 " done=%d.\n",
                   this, stream.get(), stream->StreamId(),
                   static_cast<uint32_t>(rv), stream->Done()));
              // We need to keep the transaction, so we can use it to remove the
              // stream from mStreamTransactionHash.
              nsAHttpTransaction* trans = stream->Transaction();
              if (mStreamTransactionHash.Contains(trans)) {
                CloseStream(stream, (rv == NS_BINDING_RETARGETED)
                                        ? NS_BINDING_RETARGETED
                                        : NS_OK);
                mStreamTransactionHash.Remove(trans);
              } else {
                stream->GetHttp3WebTransportSession()->TransactionIsDone(
                    (rv == NS_BINDING_RETARGETED) ? NS_BINDING_RETARGETED
                                                  : NS_OK);
              }
              break;
            }

            if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
              LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this,
                   static_cast<uint32_t>(rv)));
              return rv;
            }
          } break;
          case WebTransportEventExternal::Tag::SessionClosed: {
            uint64_t id = event.web_transport._0.session_closed.stream_id;
            LOG(
                ("Http3Session::ProcessEvents - WebTransport SessionClosed "
                 " sessionId=0x%" PRIx64,
                 id));
            RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(id);
            if (!stream) {
              LOG(
                  ("Http3Session::ProcessEvents - WebTransport SessionClosed - "
                   "stream not found "
                   "stream_id=0x%" PRIx64 " [this=%p].",
                   id, this));
              break;
            }

            RefPtr<Http3WebTransportSession> wt =
                stream->GetHttp3WebTransportSession();
            MOZ_RELEASE_ASSERT(wt, "It must be a WebTransport session");

            bool cleanly = false;

            // TODO we do not handle the case when a WebTransport session stream
            // is closed before headers are sent.
            SessionCloseReasonExternal& reasonExternal =
                event.web_transport._0.session_closed.reason;
            uint32_t status = 0;
            nsCString reason = ""_ns;
            if (reasonExternal.tag == SessionCloseReasonExternal::Tag::Error) {
              status = reasonExternal.error._0;
            } else if (reasonExternal.tag ==
                       SessionCloseReasonExternal::Tag::Status) {
              status = reasonExternal.status._0;
              cleanly = true;
            } else {
              status = reasonExternal.clean._0;
              reason.Assign(reinterpret_cast<const char*>(data.Elements()),
                            data.Length());
              cleanly = true;
            }
            LOG(("reason.tag=%u err=%u data=%s\n",
                 static_cast<uint32_t>(reasonExternal.tag), status,
                 reason.get()));
            wt->OnSessionClosed(cleanly, status, reason);

          } break;
          case WebTransportEventExternal::Tag::NewStream: {
            LOG(
                ("Http3Session::ProcessEvents - WebTransport NewStream "
                 "streamId=0x%" PRIx64 " sessionId=0x%" PRIx64,
                 event.web_transport._0.new_stream.stream_id,
                 event.web_transport._0.new_stream.session_id));
            uint64_t sessionId = event.web_transport._0.new_stream.session_id;
            RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(sessionId);
            if (!stream) {
              LOG(
                  ("Http3Session::ProcessEvents - WebTransport NewStream - "
                   "session not found "
                   "sessionId=0x%" PRIx64 " [this=%p].",
                   sessionId, this));
              break;
            }

            RefPtr<Http3WebTransportSession> wt =
                stream->GetHttp3WebTransportSession();
            if (!wt) {
              break;
            }

            RefPtr<Http3WebTransportStream> wtStream =
                wt->OnIncomingWebTransportStream(
                    event.web_transport._0.new_stream.stream_type,
                    event.web_transport._0.new_stream.stream_id);
            if (!wtStream) {
              break;
            }

            // WebTransportStream is managed by Http3Session now.
            mWebTransportStreams.AppendElement(wtStream);
            mWebTransportStreamToSessionMap.InsertOrUpdate(wtStream->StreamId(),
                                                           wt->StreamId());
            mStreamIdHash.InsertOrUpdate(wtStream->StreamId(),
                                         std::move(wtStream));
          } break;
          case WebTransportEventExternal::Tag::Datagram:
            LOG(
                ("Http3Session::ProcessEvents - "
                 "WebTransportEventExternal::Tag::Datagram [this=%p]",
                 this));
            uint64_t sessionId = event.web_transport._0.datagram.session_id;
            RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(sessionId);
            if (!stream) {
              LOG(
                  ("Http3Session::ProcessEvents - WebTransport Datagram - "
                   "session not found "
                   "sessionId=0x%" PRIx64 " [this=%p].",
                   sessionId, this));
              break;
            }

            RefPtr<Http3WebTransportSession> wt =
                stream->GetHttp3WebTransportSession();
            if (!wt) {
              break;
            }

            wt->OnDatagramReceived(std::move(data));
            break;
        }
      } break;
      case Http3Event::Tag::ConnectUdp: {
        switch (event.connect_udp._0.tag) {
          case ConnectUdpEventExternal::Tag::Negotiated:
            LOG(("Http3Session::ProcessEvents - ConnectUdp Negotiated %d",
                 event.connect_udp._0.negotiated._0));
            FinishNegotiation(ExtendedConnectKind::ConnectUDP,
                              event.connect_udp._0.negotiated._0);
            break;
          case ConnectUdpEventExternal::Tag::Session: {
            MOZ_ASSERT(mState == CONNECTED);

            uint64_t id = event.connect_udp._0.session._0;
            LOG(
                ("Http3Session::ProcessEvents - ConnectUdp "
                 " streamId=0x%" PRIx64,
                 id));
            RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(id);
            if (!stream) {
              LOG(
                  ("Http3Session::ProcessEvents - ConnectUdp Session - "
                   "stream not found "
                   "stream_id=0x%" PRIx64 " [this=%p].",
                   id, this));
              break;
            }

            MOZ_RELEASE_ASSERT(stream->GetHttp3ConnectUDPStream(),
                               "It must be a ConnectUdp session");
            stream->SetResponseHeaders(data, false, false);

            rv = stream->WriteSegments();

            LOG(("rv=%x", static_cast<uint32_t>(rv)));

            if (ASpdySession::SoftStreamError(rv) || stream->Done()) {
              LOG3(
                  ("Http3Session::ProcessSingleTransactionRead session=%p "
                   "stream=%p "
                   "0x%" PRIx64 " cleanup stream rv=0x%" PRIx32 " done=%d.\n",
                   this, stream.get(), stream->StreamId(),
                   static_cast<uint32_t>(rv), stream->Done()));
              // We need to keep the transaction, so we can use it to remove the
              // stream from mStreamTransactionHash.
              nsAHttpTransaction* trans = stream->Transaction();
              if (mStreamTransactionHash.Contains(trans)) {
                CloseStream(stream, (rv == NS_BINDING_RETARGETED)
                                        ? NS_BINDING_RETARGETED
                                        : NS_OK);
                mStreamTransactionHash.Remove(trans);
              } else {
                stream->GetHttp3ConnectUDPStream()->TransactionIsDone(
                    (rv == NS_BINDING_RETARGETED) ? NS_BINDING_RETARGETED
                                                  : NS_OK);
              }
              break;
            }

            if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
              LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this,
                   static_cast<uint32_t>(rv)));
              return rv;
            }
          } break;
          case ConnectUdpEventExternal::Tag::SessionClosed: {
            uint64_t id = event.connect_udp._0.session_closed.stream_id;
            LOG(
                ("Http3Session::ProcessEvents - connect_udp SessionClosed "
                 " sessionId=0x%" PRIx64,
                 id));
            RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(id);
            if (!stream) {
              LOG(
                  ("Http3Session::ProcessEvents - connect_udp SessionClosed - "
                   "stream not found "
                   "stream_id=0x%" PRIx64 " [this=%p].",
                   id, this));
              break;
            }

            RefPtr<Http3ConnectUDPStream> connectUDPStream =
                stream->GetHttp3ConnectUDPStream();
            MOZ_RELEASE_ASSERT(connectUDPStream,
                               "It must be a ConnectUDP stream");

            // TODO we do not handle the case when a ConnectUDP session stream
            // is closed before headers are sent.
            SessionCloseReasonExternal& reasonExternal =
                event.connect_udp._0.session_closed.reason;
            uint32_t status = 0;
            nsCString reason = ""_ns;
            if (reasonExternal.tag == SessionCloseReasonExternal::Tag::Error) {
              status = reasonExternal.error._0;
            } else if (reasonExternal.tag ==
                       SessionCloseReasonExternal::Tag::Status) {
              status = reasonExternal.status._0;
            } else {
              status = reasonExternal.clean._0;
              reason.Assign(reinterpret_cast<const char*>(data.Elements()),
                            data.Length());
            }
            LOG(("reason.tag=%u err=%u data=%s\n",
                 static_cast<uint32_t>(reasonExternal.tag), status,
                 reason.get()));
            CloseStream(connectUDPStream,
                        status == 0 ? NS_OK : NS_ERROR_FAILURE);
          } break;
          case ConnectUdpEventExternal::Tag::Datagram:
            LOG(("Http3Session::ProcessEvents - ConnectUdp Datagram"));
            uint64_t streamId = event.connect_udp._0.datagram.session_id;
            RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(streamId);
            if (!stream) {
              LOG(
                  ("Http3Session::ProcessEvents - ConnectUdp Datagram - "
                   "stream not found "
                   "streamId=0x%" PRIx64 " [this=%p].",
                   streamId, this));
              break;
            }

            RefPtr<Http3ConnectUDPStream> tunnelStream =
                stream->GetHttp3ConnectUDPStream();
            if (!tunnelStream) {
              break;
            }
            tunnelStream->OnDatagramReceived(std::move(data));
            break;
        }
      } break;
      default:
        break;
    }
    // Delete previous content of data
    data.ClearAndRetainStorage();
    rv = mHttp3Connection->GetEvent(&event, data);
    if (NS_FAILED(rv)) {
      LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this,
           static_cast<uint32_t>(rv)));
      return rv;
    }
  }

  return NS_OK;
}  // namespace net

// This function may return a socket error.
// It will not return an error if socket error is
// NS_BASE_STREAM_WOULD_BLOCK.
// A Caller of this function will close the Http3 connection
// if this function returns an error.
// Callers are:
//   1) HttpConnectionUDP::OnQuicTimeoutExpired
//   2) HttpConnectionUDP::SendData ->
//      Http3Session::SendData
nsresult Http3Session::ProcessOutput(nsIUDPSocket* socket) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(mUdpConn);

  LOG(("Http3Session::ProcessOutput reader=%p, [this=%p]", mUdpConn.get(),
       this));

  if (!socket || socket->IsSocketClosed()) {
    MOZ_DIAGNOSTIC_ASSERT(false, "UDP socket should still be open");
    return NS_ERROR_UNEXPECTED;
  }

  if (mUseNSPRForIO) {
    mSocket = socket;
    nsresult rv = mHttp3Connection->ProcessOutputAndSendUseNSPRForIO(
        this,
        [](void* aContext, uint16_t aFamily, const uint8_t* aAddr,
           uint16_t aPort, const uint8_t* aData, uint32_t aLength) {
          Http3Session* self = (Http3Session*)aContext;

          uint32_t written = 0;
          NetAddr addr;
          if (NS_FAILED(RawBytesToNetAddr(aFamily, aAddr, aPort, &addr))) {
            return NS_OK;
          }

          LOG3(
              ("Http3Session::ProcessOutput sending packet with %u bytes to %s "
               "port=%d [this=%p].",
               aLength, addr.ToString().get(), aPort, self));

          nsresult rv =
              self->mSocket->SendWithAddress(&addr, aData, aLength, &written);

          LOG(("Http3Session::ProcessOutput sending packet rv=%d osError=%d",
               static_cast<int32_t>(rv), NS_FAILED(rv) ? PR_GetOSError() : 0));
          if (NS_FAILED(rv) && (rv != NS_BASE_STREAM_WOULD_BLOCK)) {
            if (rv == NS_ERROR_OUT_OF_MEMORY) {
              // NSPR maps ENOBUFS to PR_INSUFFICIENT_RESOURCES_ERROR, which
              // becomes NS_ERROR_OUT_OF_MEMORY. On macOS/BSD, ENOBUFS means
              // the NIC transmit queue is momentarily full.
              LOG(
                  ("Http3Session::ProcessOutput ENOBUFS (transient), dropping "
                   "datagram [this=%p]",
                   self));
            } else {
              self->mSocketError = rv;
              // If there was another error, return from here. We do not need to
              // set a timer, because we will close the connection.
              return rv;
            }
          }
          self->mTotalBytesWritten += aLength;
          self->mLastWriteTime = PR_IntervalNow();
          return NS_OK;
        },
        [](void* aContext, uint64_t timeout) {
          Http3Session* self = (Http3Session*)aContext;
          self->SetupTimer(timeout);
        });
    mSocket = nullptr;
    return rv;
  }

  // Not using NSPR.

  auto rv = mHttp3Connection->ProcessOutputAndSend(
      this, [](void* aContext, uint64_t timeout) {
        Http3Session* self = (Http3Session*)aContext;
        self->SetupTimer(timeout);
      });
  if (rv.result == NS_BASE_STREAM_WOULD_BLOCK) {
    // The OS buffer was full. Tell the UDP socket to poll for
    // write-availability.
    socket->EnableWritePoll();
  } else if (NS_FAILED(rv.result)) {
    mSocketError = rv.result;
    // If there was an error return from here. We do not need to set a timer,
    // because we will close the connection.
    return rv.result;
  }
  if (rv.bytes_written != 0) {
    mTotalBytesWritten += rv.bytes_written;
    mLastWriteTime = PR_IntervalNow();
    socket->AddOutputBytes(rv.bytes_written);
  }

  return NS_OK;
}

// This is only called when timer expires.
// It is called by HttpConnectionUDP::OnQuicTimeout.
// If tihs function returns an error OnQuicTimeout will handle the error
// properly and close the connection.
nsresult Http3Session::ProcessOutputAndEvents(nsIUDPSocket* socket) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  MOZ_ASSERT(mTimerShouldTrigger);

  // Check if session has been stuck in ZERORTT state for too long
  if (mState == ZERORTT) {
    MOZ_ASSERT(mZeroRttStarted);
    uint32_t timeout = StaticPrefs::network_http_http3_0rtt_timeout();
    if (timeout > 0) {
      TimeDuration elapsed = TimeStamp::Now() - mZeroRttStarted;
      if (elapsed.ToMilliseconds() > timeout) {
        LOG(
            ("Http3Session %p stuck in ZERORTT for %.2fms (timeout=%ums), "
             "closing connection",
             this, elapsed.ToMilliseconds(), timeout));
        return NS_ERROR_NET_TIMEOUT;
      }
    }
  }

  if (Telemetry::CanRecordPrereleaseData()) {
    auto now = TimeStamp::Now();
    if (mTimerShouldTrigger > now) {
      // See bug 1935459
      glean::http3::timer_delayed.AccumulateRawDuration(nullptr);
    } else {
      glean::http3::timer_delayed.AccumulateRawDuration(now -
                                                        mTimerShouldTrigger);
    }
  }

  mTimerShouldTrigger = TimeStamp();

  nsresult rv = SendData(socket);
  if (NS_FAILED(rv)) {
    return rv;
  }
  return NS_OK;
}

NS_IMPL_ISUPPORTS(Http3Session::OnQuicTimeout, nsITimerCallback, nsINamed)

Http3Session::OnQuicTimeout::OnQuicTimeout(HttpConnectionUDP* aConnection)
    : mConnection(aConnection) {
  MOZ_ASSERT(mConnection);
}

NS_IMETHODIMP
Http3Session::OnQuicTimeout::Notify(nsITimer* timer) {
  mConnection->OnQuicTimeoutExpired();
  return NS_OK;
}

NS_IMETHODIMP
Http3Session::OnQuicTimeout::GetName(nsACString& aName) {
  aName.AssignLiteral("net::HttpConnectionUDP::OnQuicTimeout");
  return NS_OK;
}

void Http3Session::SetupTimer(uint64_t aTimeout) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  // UINT64_MAX indicated a no-op from neqo, which only happens when a
  // connection is in or going to be Closed state.
  if (aTimeout == UINT64_MAX) {
    return;
  }

  // If we're in ZERORTT state, ensure the timeout doesn't exceed the
  // 0-RTT timeout to prevent the session from being closed later than expected.
  if (mState == ZERORTT) {
    MOZ_ASSERT(mZeroRttStarted);
    uint32_t zeroRttTimeout = StaticPrefs::network_http_http3_0rtt_timeout();
    if (zeroRttTimeout > 0) {
      TimeDuration elapsed = TimeStamp::Now() - mZeroRttStarted;
      uint64_t remainingMs =
          static_cast<uint64_t>(zeroRttTimeout - elapsed.ToMilliseconds());

      if (elapsed.ToMilliseconds() < zeroRttTimeout && aTimeout > remainingMs) {
        LOG3(("Http3Session::SetupTimer capping timeout from %" PRIu64
              "ms to %" PRIu64 "ms (0-RTT timeout remaining) [this=%p].",
              aTimeout, remainingMs, this));
        aTimeout = remainingMs;
      }
    }
  }

  LOG3(
      ("Http3Session::SetupTimer to %" PRIu64 "ms [this=%p].", aTimeout, this));

  // Remember the time when the timer should trigger.
  mTimerShouldTrigger =
      TimeStamp::Now() + TimeDuration::FromMilliseconds(aTimeout);

  if (!mTimerCallback) {
    // We can keep the same callback object for all our lifetime.
    mTimerCallback = MakeRefPtr<OnQuicTimeout>(mUdpConn);
  }

  if (!mTimer) {
    // This can only fail on OOM and we'd crash.
    mTimer = NS_NewTimer();
  }

  DebugOnly<nsresult> rv = mTimer->InitWithCallback(mTimerCallback, aTimeout,
                                                    nsITimer::TYPE_ONE_SHOT);
  // There is no meaningful error handling we can do here. But an error here
  // should only be possible if the timer thread did already shut down.
  MOZ_ASSERT(NS_SUCCEEDED(rv));
}

bool Http3Session::AddStream(nsAHttpTransaction* aHttpTransaction,
                             int32_t aPriority,
                             nsIInterfaceRequestor* aCallbacks) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  nsHttpTransaction* trans = aHttpTransaction->QueryHttpTransaction();

  bool firstStream = false;
  if (!mConnection) {
    // Get the connection from the first transaction.
    mConnection = aHttpTransaction->Connection();
    firstStream = true;
  }

  // Make sure we report the connectStart
  auto reportConnectStart = MakeScopeExit([&] {
    if (firstStream) {
      OnTransportStatus(nullptr, NS_NET_STATUS_CONNECTING_TO, 0);
    }
  });

  if (IsClosing()) {
    LOG3(
        ("Http3Session::AddStream %p atrans=%p trans=%p session unusable - "
         "resched.\n",
         this, aHttpTransaction, trans));
    aHttpTransaction->SetConnection(nullptr);
    nsresult rv = gHttpHandler->InitiateTransaction(trans, trans->Priority());
    if (NS_FAILED(rv)) {
      LOG3(
          ("Http3Session::AddStream %p atrans=%p trans=%p failed to initiate "
           "transaction (0x%" PRIx32 ").\n",
           this, aHttpTransaction, trans, static_cast<uint32_t>(rv)));
    }
    return true;
  }

  aHttpTransaction->SetConnection(this);
  aHttpTransaction->OnActivated();
  // reset the read timers to wash away any idle time
  mLastWriteTime = PR_IntervalNow();

  ClassOfService cos;
  if (trans) {
    cos = trans->GetClassOfService();
  }

  Http3StreamBase* stream = nullptr;

  if (trans && mConnInfo->IsHttp3ProxyConnection() && !mIsInTunnel) {
    LOG3(("Http3Session::AddStream new connect-udp stream %p atrans=%p.\n",
          this, aHttpTransaction));
    stream = new Http3ConnectUDPStream(aHttpTransaction, this,
                                       NS_GetCurrentThread());
    if (mConnInfo->GetIsTrrServiceChannel()) {
      stream->GetHttp3ConnectUDPStream()->MarkAsTRRServiceChannel();
    }
  } else if (trans && trans->IsForWebTransport()) {
    LOG3(("Http3Session::AddStream new  WeTransport session %p atrans=%p.\n",
          this, aHttpTransaction));
    stream = new Http3WebTransportSession(aHttpTransaction, this);
    mHasWebTransportSession = true;
  } else {
    LOG3(("Http3Session::AddStream %p atrans=%p.\n", this, aHttpTransaction));
    stream = new Http3Stream(aHttpTransaction, this, cos, mCurrentBrowserId);
  }

  mStreamTransactionHash.InsertOrUpdate(aHttpTransaction, RefPtr{stream});

  if (mState == ZERORTT) {
    if (!stream->Do0RTT()) {
      LOG(("Http3Session %p will not get early data from Http3Stream %p", this,
           stream));
      if (!mCannotDo0RTTStreams.Contains(stream)) {
        mCannotDo0RTTStreams.AppendElement(stream);
      }
      if (stream->GetHttp3WebTransportSession()) {
        DeferIfNegotiating(ExtendedConnectKind::WebTransport, stream);
      } else if (stream->GetHttp3ConnectUDPStream()) {
        DeferIfNegotiating(ExtendedConnectKind::ConnectUDP, stream);
      }
      return true;
    }
    m0RTTStreams.AppendElement(stream);
  }

  if (stream->GetHttp3WebTransportSession()) {
    if (DeferIfNegotiating(ExtendedConnectKind::WebTransport, stream)) {
      return true;
    }
  } else if (stream->GetHttp3ConnectUDPStream()) {
    if (DeferIfNegotiating(ExtendedConnectKind::ConnectUDP, stream)) {
      return true;
    }
  }

  if (!mFirstHttpTransaction && !IsConnected()) {
    mFirstHttpTransaction = aHttpTransaction->QueryHttpTransaction();
    LOG3(("Http3Session::AddStream first session=%p trans=%p ", this,
          mFirstHttpTransaction.get()));
  }
  StreamReadyToWrite(stream);

  return true;
}

void Http3Session::SwapTransaction(nsAHttpTransaction* aOld,
                                   nsAHttpTransaction* aNew) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(aOld && aNew);
  RefPtr<Http3StreamBase> stream = mStreamTransactionHash.Get(aOld);
  if (!stream) {
    LOG3(("Http3Session::SwapTransaction %p aOld=%p not in hash", this, aOld));
    return;
  }
  LOG3(("Http3Session::SwapTransaction %p aOld=%p -> aNew=%p stream=%p", this,
        aOld, aNew, stream.get()));
  stream->SetTransaction(aNew);
  mStreamTransactionHash.Remove(aOld);
  mStreamTransactionHash.InsertOrUpdate(aNew, std::move(stream));
  // mFirstHttpTransaction is tracked by nsHttpTransaction pointer; if it
  // was the shim, replace it with the real txn too.
  if (mFirstHttpTransaction &&
      mFirstHttpTransaction.get() == aOld->QueryHttpTransaction()) {
    mFirstHttpTransaction = aNew->QueryHttpTransaction();
  }
}

bool Http3Session::DeferIfNegotiating(ExtendedConnectKind aKind,
                                      Http3StreamBase* aStream) {
  auto& st = ExtState(aKind);
  if (st.mStatus == NEGOTIATING) {
    if (!st.mWaiters.Contains(aStream)) {
      LOG(("waiting for negotiation"));
      st.mWaiters.AppendElement(aStream);
    }
    return true;
  }
  return false;
}

void Http3Session::FinishNegotiation(ExtendedConnectKind aKind, bool aSuccess) {
  auto& st = ExtState(aKind);
  if (st.mWaiters.IsEmpty()) {
    st.mStatus = aSuccess ? SUCCEEDED : FAILED;
    return;
  }

  MOZ_ASSERT(st.mStatus == NEGOTIATING);
  st.mStatus = aSuccess ? SUCCEEDED : FAILED;

  for (size_t i = 0; i < st.mWaiters.Length(); ++i) {
    if (st.mWaiters[i]) {
      mReadyForWrite.Push(st.mWaiters[i]);
    }
  }
  st.mWaiters.Clear();
  MaybeResumeSend();
}

bool Http3Session::CanReuse() {
  // TODO: we assume "pooling" is disabled here, so we don't allow this session
  // to be reused. "pooling" will be implemented in bug 1815735.
  return CanSendData() && !(mGoawayReceived || mShouldClose) &&
         !mHasWebTransportSession;
}

void Http3Session::QueueStream(Http3StreamBase* stream) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(!stream->Queued());

  LOG3(("Http3Session::QueueStream %p stream %p queued.", this, stream));

  stream->SetQueued(true);
  mQueuedStreams.Push(stream);
}

void Http3Session::ProcessPending() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  RefPtr<Http3StreamBase> stream;
  while ((stream = mQueuedStreams.PopFront())) {
    LOG3(("Http3Session::ProcessPending %p stream %p woken from queue.", this,
          stream.get()));
    MOZ_ASSERT(stream->Queued());
    stream->SetQueued(false);
    mReadyForWrite.Push(stream);
  }
  MaybeResumeSend();
}

static void RemoveStreamFromQueue(Http3StreamBase* aStream,
                                  nsRefPtrDeque<Http3StreamBase>& queue) {
  size_t size = queue.GetSize();
  for (size_t count = 0; count < size; ++count) {
    RefPtr<Http3StreamBase> stream = queue.PopFront();
    if (stream != aStream) {
      queue.Push(stream);
    }
  }
}

void Http3Session::RemoveStreamFromQueues(Http3StreamBase* aStream) {
  RemoveStreamFromQueue(aStream, mReadyForWrite);
  RemoveStreamFromQueue(aStream, mQueuedStreams);
  mSlowConsumersReadyForRead.RemoveElement(aStream);
}

// This is called by Http3Stream::OnReadSegment.
// ProcessOutput will be called in Http3Session::ReadSegment that
// calls Http3Stream::OnReadSegment.
nsresult Http3Session::TryActivating(
    const nsACString& aMethod, const nsACString& aScheme,
    const nsACString& aAuthorityHeader, const nsACString& aPathQuery,
    const nsACString& aHeaders, uint64_t* aStreamId, Http3StreamBase* aStream) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(*aStreamId == UINT64_MAX);

  LOG(("Http3Session::TryActivating [stream=%p, this=%p state=%d]", aStream,
       this, mState));

  if (IsClosing()) {
    if (NS_FAILED(mError)) {
      return mError;
    }
    return NS_ERROR_FAILURE;
  }

  if (aStream->Queued()) {
    LOG3(("Http3Session::TryActivating %p stream=%p already queued.\n", this,
          aStream));
    return NS_BASE_STREAM_WOULD_BLOCK;
  }

  if (mState == ZERORTT) {
    if (!aStream->Do0RTT()) {
      // Stream can't do 0RTT - queue it for activation when the session
      // reaches CONNECTED state via Finish0Rtt.
      if (!mCannotDo0RTTStreams.Contains(aStream)) {
        LOG(("Http3Session %p queuing stream %p for post-0RTT activation", this,
             aStream));
        mCannotDo0RTTStreams.AppendElement(aStream);
      }
      return NS_BASE_STREAM_WOULD_BLOCK;
    }
    mHad0RttStream = true;
  }

  nsresult rv = NS_OK;
  // The order of these checks is important: Http3StreamTunnel inherits from
  // Http3Stream, so a tunnel will also match conditions for a regular stream.
  // Ensure we handle Http3StreamTunnel cases before generic Http3Stream logic.
  if (RefPtr<Http3StreamTunnel> streamTunnel =
          aStream->GetHttp3StreamTunnel()) {
    rv = mHttp3Connection->Connect(aAuthorityHeader, aHeaders, aStreamId, 3,
                                   false);
  } else if (RefPtr<Http3Stream> httpStream = aStream->GetHttp3Stream()) {
    rv = mHttp3Connection->Fetch(
        aMethod, aScheme, aAuthorityHeader, aPathQuery, aHeaders, aStreamId,
        httpStream->PriorityUrgency(), httpStream->PriorityIncremental());
  } else if (RefPtr<Http3ConnectUDPStream> udpStream =
                 aStream->GetHttp3ConnectUDPStream()) {
    if (DeferIfNegotiating(ExtendedConnectKind::ConnectUDP, aStream)) {
      return NS_BASE_STREAM_WOULD_BLOCK;
    }
    rv = mHttp3Connection->CreateConnectUdp(aAuthorityHeader, aPathQuery,
                                            aHeaders, aStreamId);
  } else {
    MOZ_RELEASE_ASSERT(aStream->GetHttp3WebTransportSession(),
                       "It must be a WebTransport session");
    // Don't call CreateWebTransport if we are still waiting for the negotiation
    // result.
    if (DeferIfNegotiating(ExtendedConnectKind::WebTransport, aStream)) {
      return NS_BASE_STREAM_WOULD_BLOCK;
    }
    rv = mHttp3Connection->CreateWebTransport(aAuthorityHeader, aPathQuery,
                                              aHeaders, aStreamId);
  }

  if (NS_FAILED(rv)) {
    LOG(("Http3Session::TryActivating returns error=0x%" PRIx32 "[stream=%p, "
         "this=%p]",
         static_cast<uint32_t>(rv), aStream, this));
    if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
      LOG3(
          ("Http3Session::TryActivating %p stream=%p no room for more "
           "concurrent streams\n",
           this, aStream));
      mTransactionsBlockedByStreamLimitCount++;
      if (mQueuedStreams.GetSize() == 0) {
        mBlockedByStreamLimitCount++;
      }
      QueueStream(aStream);
      return rv;
    }

    // Previously we always returned NS_OK here, which caused the
    // transaction to wait until the quic connection timed out
    // after which it was retried without quic.
    if (StaticPrefs::network_http_http3_fallback_to_h2_on_error()) {
      return NS_ERROR_HTTP2_FALLBACK_TO_HTTP1;
    }

    return rv;
  }

  LOG(("Http3Session::TryActivating streamId=0x%" PRIx64
       " for stream=%p [this=%p].",
       *aStreamId, aStream, this));

  MOZ_ASSERT(*aStreamId != UINT64_MAX);

  if (mTransactionCount > 0 && mStreamIdHash.IsEmpty()) {
    MOZ_ASSERT(mConnectionIdleStart);
    MOZ_ASSERT(mFirstStreamIdReuseIdleConnection.isNothing());

    mConnectionIdleEnd = TimeStamp::Now();
    mFirstStreamIdReuseIdleConnection = Some(*aStreamId);
  }
  mStreamIdHash.InsertOrUpdate(*aStreamId, RefPtr{aStream});
  mTransactionCount++;

  return NS_OK;
}

// This is called by Http3WebTransportStream::OnReadSegment.
// TODO: this function is almost the same as TryActivating().
// We should try to reduce the duplicate code.
nsresult Http3Session::TryActivatingWebTransportStream(
    uint64_t* aStreamId, Http3StreamBase* aStream) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(*aStreamId == UINT64_MAX);

  LOG(
      ("Http3Session::TryActivatingWebTransportStream [stream=%p, this=%p "
       "state=%d]",
       aStream, this, mState));

  if (IsClosing()) {
    if (NS_FAILED(mError)) {
      return mError;
    }
    return NS_ERROR_FAILURE;
  }

  if (aStream->Queued()) {
    LOG3(
        ("Http3Session::TryActivatingWebTransportStream %p stream=%p already "
         "queued.\n",
         this, aStream));
    return NS_BASE_STREAM_WOULD_BLOCK;
  }

  nsresult rv = NS_OK;
  RefPtr<Http3WebTransportStream> wtStream =
      aStream->GetHttp3WebTransportStream();
  MOZ_RELEASE_ASSERT(wtStream, "It must be a WebTransport stream");
  rv = mHttp3Connection->CreateWebTransportStream(
      wtStream->SessionId(), wtStream->StreamType(), aStreamId);

  if (NS_FAILED(rv)) {
    LOG((
        "Http3Session::TryActivatingWebTransportStream returns error=0x%" PRIx32
        "[stream=%p, "
        "this=%p]",
        static_cast<uint32_t>(rv), aStream, this));
    if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
      LOG3(
          ("Http3Session::TryActivatingWebTransportStream %p stream=%p no room "
           "for more "
           "concurrent streams\n",
           this, aStream));
      QueueStream(aStream);
      return rv;
    }

    return rv;
  }

  LOG(("Http3Session::TryActivatingWebTransportStream streamId=0x%" PRIx64
       " for stream=%p [this=%p].",
       *aStreamId, aStream, this));

  MOZ_ASSERT(*aStreamId != UINT64_MAX);

  RefPtr<Http3StreamBase> session = mStreamIdHash.Get(wtStream->SessionId());
  MOZ_ASSERT(session);
  Http3WebTransportSession* wtSession = session->GetHttp3WebTransportSession();
  MOZ_ASSERT(wtSession);

  wtSession->RemoveWebTransportStream(wtStream);

  // WebTransportStream is managed by Http3Session now.
  mWebTransportStreams.AppendElement(wtStream);
  mWebTransportStreamToSessionMap.InsertOrUpdate(*aStreamId,
                                                 session->StreamId());
  mStreamIdHash.InsertOrUpdate(*aStreamId, std::move(wtStream));
  return NS_OK;
}

// This is only called by Http3Stream::OnReadSegment.
// ProcessOutput will be called in Http3Session::ReadSegment that
// calls Http3Stream::OnReadSegment.
void Http3Session::CloseSendingSide(uint64_t aStreamId) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  mHttp3Connection->CloseStream(aStreamId);
}

// This is only called by Http3Stream::OnReadSegment.
// ProcessOutput will be called in Http3Session::ReadSegment that
// calls Http3Stream::OnReadSegment.
nsresult Http3Session::SendRequestBody(uint64_t aStreamId, const char* buf,
                                       uint32_t count, uint32_t* countRead) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  nsresult rv = mHttp3Connection->SendRequestBody(
      aStreamId, (const uint8_t*)buf, count, countRead);
  if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
    mTransactionsSenderBlockedByFlowControlCount++;
  } else if (NS_FAILED(rv)) {
    // Ignore this error. This may happen if some events are not handled yet.
    // TODO we may try to add an assertion here.
    // We will pretend that sender is blocked, that will cause the caller to
    // stop trying.
    *countRead = 0;
    rv = NS_BASE_STREAM_WOULD_BLOCK;
  }

  MOZ_ASSERT((*countRead != 0) || NS_FAILED(rv));
  return rv;
}

void Http3Session::ResetOrStopSendingRecvd(uint64_t aStreamId, uint64_t aError,
                                           ResetType aType) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  uint64_t sessionId = 0;
  if (mWebTransportStreamToSessionMap.Get(aStreamId, &sessionId)) {
    uint8_t wtError = Http3ErrorToWebTransportError(aError);
    nsresult rv = GetNSResultFromWebTransportError(wtError);

    RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(aStreamId);
    if (stream) {
      if (aType == RESET) {
        stream->SetRecvdReset();
      }
      RefPtr<Http3WebTransportStream> wtStream =
          stream->GetHttp3WebTransportStream();
      if (wtStream) {
        CloseWebTransportStream(wtStream, rv);
      }
    }

    RefPtr<Http3StreamBase> session = mStreamIdHash.Get(sessionId);
    if (session) {
      Http3WebTransportSession* wtSession =
          session->GetHttp3WebTransportSession();
      MOZ_ASSERT(wtSession);
      if (wtSession) {
        if (aType == RESET) {
          wtSession->OnStreamReset(aStreamId, rv);
        } else {
          wtSession->OnStreamStopSending(aStreamId, rv);
        }
      }
    }
    return;
  }

  RefPtr<Http3StreamBase> stream = mStreamIdHash.Get(aStreamId);
  if (!stream) {
    return;
  }

  RefPtr<Http3Stream> httpStream = stream->GetHttp3Stream();
  if (!httpStream) {
    return;
  }

  // We only handle some of Http3 error as epecial, the rest are just equivalent
  // to cancel.
  if (aError == HTTP3_APP_ERROR_VERSION_FALLBACK) {
    // We will restart the request and the alt-svc will be removed
    // automatically.
    // Also disable http3 we want http1.1.
    httpStream->Transaction()->DisableHttp3(false);
    httpStream->Transaction()->DisableSpdy();
    CloseStream(stream, NS_ERROR_NET_RESET);
  } else if (aError == HTTP3_APP_ERROR_REQUEST_REJECTED) {
    // This request was rejected because server is probably busy or going away.
    // We can restart the request using alt-svc. Without calling
    // DoNotRemoveAltSvc the alt-svc route will be removed.
    httpStream->Transaction()->DoNotRemoveAltSvc();
    CloseStream(stream, NS_ERROR_NET_RESET);
  } else if (aError == HTTP3_APP_ERROR_REQUEST_CANCELLED) {
    // The server cancelled this request; it may have had side effects, so
    // do not retry.
    CloseStream(stream, httpStream->RecvdData() ? NS_ERROR_NET_PARTIAL_TRANSFER
                                                : NS_ERROR_NET_INTERRUPT);
  } else {
    // Unrecognized application error code. Use NS_ERROR_NET_RESET so the
    // transaction restart path retries via H2/H1.
    CloseStream(stream, httpStream->RecvdData() ? NS_ERROR_NET_PARTIAL_TRANSFER
                                                : NS_ERROR_NET_RESET);
  }
}

void Http3Session::SetConnection(nsAHttpConnection* aConn) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  mConnection = aConn;
}

void Http3Session::GetSecurityCallbacks(nsIInterfaceRequestor** aOut) {
  *aOut = nullptr;
}

// TODO
void Http3Session::OnTransportStatus(nsITransport* aTransport, nsresult aStatus,
                                     int64_t aProgress) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  switch (aStatus) {
      // These should appear only once, deliver to the first
      // transaction on the session.
    case NS_NET_STATUS_RESOLVING_HOST:
    case NS_NET_STATUS_RESOLVED_HOST:
    case NS_NET_STATUS_CONNECTING_TO:
    case NS_NET_STATUS_CONNECTED_TO: {
      if (!mFirstHttpTransaction) {
        // if we still do not have a HttpTransaction store timings info in
        // a HttpConnection.
        // If some error occur it can happen that we do not have a connection.
        if (mConnection) {
          RefPtr<HttpConnectionBase> conn = mConnection->HttpConnection();
          conn->SetEvent(aStatus);
        }
      } else {
        mFirstHttpTransaction->OnTransportStatus(aTransport, aStatus,
                                                 aProgress);
      }

      if (aStatus == NS_NET_STATUS_CONNECTED_TO) {
        mFirstHttpTransaction = nullptr;
      }
      break;
    }

    default:
      // The other transport events are ignored here because there is no good
      // way to map them to the right transaction in HTTP3. Instead, the events
      // are generated again from the HTTP3 code and passed directly to the
      // correct transaction.

      // NS_NET_STATUS_SENDING_TO:
      // This is generated by the socket transport when (part) of
      // a transaction is written out
      //
      // There is no good way to map it to the right transaction in HTTP3,
      // so it is ignored here and generated separately when the request
      // is sent from Http3Stream.

      // NS_NET_STATUS_WAITING_FOR:
      // Created by nsHttpConnection when the request has been totally sent.
      // There is no good way to map it to the right transaction in HTTP3,
      // so it is ignored here and generated separately when the same
      // condition is complete in Http3Stream when there is no more
      // request body left to be transmitted.

      // NS_NET_STATUS_RECEIVING_FROM
      // Generated in Http3Stream whenever the stream reads data.

      break;
  }
}

bool Http3Session::IsDone() { return mState == CLOSED; }

nsresult Http3Session::Status() {
  MOZ_ASSERT(false, "Http3Session::Status()");
  return NS_ERROR_UNEXPECTED;
}

uint32_t Http3Session::Caps() {
  MOZ_ASSERT(false, "Http3Session::Caps()");
  return 0;
}

nsresult Http3Session::ReadSegments(nsAHttpSegmentReader* reader,
                                    uint32_t count, uint32_t* countRead) {
  MOZ_ASSERT(false, "Http3Session::ReadSegments()");
  return NS_ERROR_UNEXPECTED;
}

nsresult Http3Session::SendData(nsIUDPSocket* socket) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  RefPtr<Http3Session> self(this);

  LOG(("Http3Session::SendData [this=%p]", this));

  //   1) go through all streams/transactions that are ready to write and
  //      write their data into quic streams (no network write yet).
  //   2) call ProcessOutput that will loop until all available packets are
  //      written to a socket or the socket returns an error code.
  //   3) if we still have streams ready to write call ResumeSend()(we may
  //      still have such streams because on an stream error we return earlier
  //      to let the error be handled).
  //   4)

  nsresult rv = NS_OK;
  RefPtr<Http3StreamBase> stream;

  nsTArray<RefPtr<Http3StreamBase>> blockedStreams;

  // Step 1)
  while (CanSendData() && (stream = mReadyForWrite.PopFront())) {
    LOG(("Http3Session::SendData call ReadSegments from stream=%p [this=%p]",
         stream.get(), this));
    stream->SetInTxQueue(false);
    if (stream->BlockedByFlowControl()) {
      LOG(("stream %p blocked by flow control", stream.get()));
      blockedStreams.AppendElement(stream);
      continue;
    }
    rv = stream->ReadSegments();

    // on stream error we return earlier to let the error be handled.
    if (NS_FAILED(rv)) {
      LOG3(("Http3Session::SendData %p returns error code 0x%" PRIx32, this,
            static_cast<uint32_t>(rv)));
      MOZ_ASSERT(rv != NS_BASE_STREAM_WOULD_BLOCK);
      if (rv == NS_BASE_STREAM_WOULD_BLOCK) {  // Just in case!
        rv = NS_OK;
      } else if (ASpdySession::SoftStreamError(rv)) {
        CloseStream(stream, rv);
        LOG3(("Http3Session::SendData %p soft error override\n", this));
        rv = NS_OK;
      } else {
        break;
      }
    }
  }

  if (NS_SUCCEEDED(rv)) {
    // Step 2:
    // Call actual network write.
    rv = ProcessOutput(socket);
  }

  // Step 3:
  MaybeResumeSend();

  if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
    rv = NS_OK;
  }

  if (NS_FAILED(rv)) {
    return rv;
  }

  // Put the blocked streams back to the queue, since they are ready to write.
  for (const auto& stream : blockedStreams) {
    mReadyForWrite.Push(stream);
    stream->SetInTxQueue(true);
  }

  rv = ProcessEvents();

  // Let the connection know we sent some app data successfully.
  if (stream && NS_SUCCEEDED(rv)) {
    mUdpConn->NotifyDataWrite();
  }

  return rv;
}

void Http3Session::StreamReadyToWrite(Http3StreamBase* aStream) {
  MOZ_ASSERT(aStream);
  // Http3Session::StreamReadyToWrite can be called multiple times when we get
  // duplicate DataWrite events from neqo at the same time. In this case, we
  // only want to insert the stream in `mReadyForWrite` once.
  if (aStream->IsInTxQueue()) {
    return;
  }

  mReadyForWrite.Push(aStream);
  aStream->SetInTxQueue(true);
  if (CanSendData() && mConnection) {
    (void)mConnection->ResumeSend();
  }
}

void Http3Session::MaybeResumeSend() {
  if ((mReadyForWrite.GetSize() > 0) && CanSendData() && mConnection) {
    (void)mConnection->ResumeSend();
  }
}

nsresult Http3Session::ProcessSlowConsumers() {
  if (mSlowConsumersReadyForRead.IsEmpty()) {
    return NS_OK;
  }

  RefPtr<Http3StreamBase> slowConsumer =
      mSlowConsumersReadyForRead.ElementAt(0);
  mSlowConsumersReadyForRead.RemoveElementAt(0);

  nsresult rv = ProcessTransactionRead(slowConsumer);

  return rv;
}

nsresult Http3Session::WriteSegments(nsAHttpSegmentWriter* writer,
                                     uint32_t count, uint32_t* countWritten) {
  MOZ_ASSERT(false, "Http3Session::WriteSegments()");
  return NS_ERROR_UNEXPECTED;
}

nsresult Http3Session::RecvData(nsIUDPSocket* socket) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  RefPtr<Http3Session> self(this);

  // Process slow consumers.
  nsresult rv = ProcessSlowConsumers();
  if (NS_FAILED(rv)) {
    LOG3(("Http3Session %p ProcessSlowConsumers returns 0x%" PRIx32 "\n", this,
          static_cast<uint32_t>(rv)));
    return rv;
  }

  rv = ProcessInput(socket);
  if (NS_FAILED(rv)) {
    return rv;
  }

  rv = ProcessEvents();
  if (NS_FAILED(rv)) {
    return rv;
  }

  rv = SendData(socket);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

const uint32_t HTTP3_TELEMETRY_APP_NECKO = 42;

void Http3Session::Close(nsresult aReason) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  LOG(("Http3Session::Close [this=%p]", this));

  if (NS_FAILED(mError)) {
    CloseInternal(false);
  } else {
    mError = aReason;
    // If necko closes connection, this will map to the "closing" key and the
    // value HTTP3_TELEMETRY_APP_NECKO.
    glean::http3::connection_close_code.Get("app_closing"_ns)
        .AccumulateSingleSample(HTTP3_TELEMETRY_APP_NECKO);
    CloseInternal(true);
  }

  if (mCleanShutdown || mIsClosedByNeqo || NS_FAILED(mSocketError)) {
    // It is network-tear-down, a socker error or neqo is state CLOSED
    // (it does not need to send any more packets or wait for new packets).
    // We need to remove all references, so that
    // Http3Session will be destroyed.
    if (mTimer) {
      mTimer->Cancel();
    }
    mTimer = nullptr;
    mTimerCallback = nullptr;
    mConnection = nullptr;
    mUdpConn = nullptr;
    mState = CLOSED;
  }
  if (mConnection) {
    // resume sending to send CLOSE_CONNECTION frame.
    (void)mConnection->ResumeSend();
  }
}

void Http3Session::CloseInternal(bool aCallNeqoClose) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  if (IsClosing()) {
    return;
  }

  LOG(("Http3Session::Closing [this=%p]", this));

  // A clean pre-CONNECTED shutdown closes with a success code; only flag a
  // before-connected error when mError actually failed.
  if (mState != CONNECTED && NS_FAILED(mError)) {
    mBeforeConnectedError = true;
  }

  if (mState == ZERORTT) {
    ZeroRttTelemetry(aCallNeqoClose ? ZeroRttOutcome::USED_CONN_CLOSED_BY_NECKO
                                    : ZeroRttOutcome::USED_CONN_ERROR);
  }

  mState = CLOSING;
  Shutdown();

  if (aCallNeqoClose) {
    mHttp3Connection->Close(HTTP3_APP_ERROR_NO_ERROR);
  }

  mStreamIdHash.Clear();
  mStreamTransactionHash.Clear();
}

nsHttpConnectionInfo* Http3Session::ConnectionInfo() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  RefPtr<nsHttpConnectionInfo> ci;
  GetConnectionInfo(getter_AddRefs(ci));
  return ci.get();
}

void Http3Session::SetProxyConnectFailed() {
  MOZ_ASSERT(false, "Http3Session::SetProxyConnectFailed()");
}

const nsHttpRequestHead* Http3Session::RequestHead() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT(false,
             "Http3Session::RequestHead() "
             "should not be called after http/3 is setup");
  return nullptr;
}

uint32_t Http3Session::Http1xTransactionCount() { return 0; }

nsresult Http3Session::TakeSubTransactions(
    nsTArray<RefPtr<nsAHttpTransaction>>& outTransactions) {
  return NS_OK;
}

//-----------------------------------------------------------------------------
// Pass through methods of nsAHttpConnection
//-----------------------------------------------------------------------------

nsAHttpConnection* Http3Session::Connection() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  return mConnection;
}

nsresult Http3Session::OnHeadersAvailable(nsAHttpTransaction* transaction,
                                          nsHttpRequestHead* requestHead,
                                          nsHttpResponseHead* responseHead,
                                          bool* reset) {
  MOZ_ASSERT(mConnection);
  if (mConnection) {
    return mConnection->OnHeadersAvailable(transaction, requestHead,
                                           responseHead, reset);
  }
  return NS_OK;
}

bool Http3Session::IsReused() {
  if (mConnection) {
    return mConnection->IsReused();
  }
  return true;
}

nsresult Http3Session::PushBack(const char* buf, uint32_t len) {
  return NS_ERROR_UNEXPECTED;
}

already_AddRefed<HttpConnectionBase> Http3Session::TakeHttpConnection() {
  LOG(("Http3Session::TakeHttpConnection %p", this));
  return nullptr;
}

already_AddRefed<HttpConnectionBase> Http3Session::HttpConnection() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  if (mConnection) {
    return mConnection->HttpConnection();
  }
  return nullptr;
}

void Http3Session::CloseTransaction(nsAHttpTransaction* aTransaction,
                                    nsresult aResult) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG3(("Http3Session::CloseTransaction %p %p 0x%" PRIx32, this, aTransaction,
        static_cast<uint32_t>(aResult)));

  // Generally this arrives as a cancel event from the connection manager.

  // need to find the stream and call CloseStream() on it.
  RefPtr<Http3StreamBase> stream = mStreamTransactionHash.Get(aTransaction);
  if (!stream) {
    LOG3(("Http3Session::CloseTransaction %p %p 0x%" PRIx32 " - not found.",
          this, aTransaction, static_cast<uint32_t>(aResult)));
    return;
  }
  LOG3(
      ("Http3Session::CloseTransaction probably a cancel. this=%p, "
       "trans=%p, result=0x%" PRIx32 ", streamId=0x%" PRIx64 " stream=%p",
       this, aTransaction, static_cast<uint32_t>(aResult), stream->StreamId(),
       stream.get()));
  CloseStream(stream, aResult);
  if (mConnection) {
    (void)mConnection->ResumeSend();
  }
}

void Http3Session::CloseStream(Http3StreamBase* aStream, nsresult aResult) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  if (aStream->Closed()) {
    LOG(("Http3Session::CloseStream aStream %p already closed", aStream));
    return;
  }

  RefPtr<Http3WebTransportStream> wtStream =
      aStream->GetHttp3WebTransportStream();
  if (wtStream) {
    CloseWebTransportStream(wtStream, aResult);
    return;
  }

  RefPtr<Http3Stream> httpStream = aStream->GetHttp3Stream();
  if (httpStream && !httpStream->RecvdFin() && !httpStream->RecvdReset() &&
      httpStream->HasStreamId()) {
    mHttp3Connection->CancelFetch(httpStream->StreamId(),
                                  HTTP3_APP_ERROR_REQUEST_CANCELLED);
  }

  if ((NS_SUCCEEDED(aResult) || NS_BASE_STREAM_CLOSED == aResult) &&
      mConnInfo->GetIsTrrServiceChannel()) {
    // save time of last successful response
    mLastTRRResponseTime = TimeStamp::Now();
    mTrrStreams++;
  }

  aStream->Close(aResult);
  CloseStreamInternal(aStream, aResult);
}

void Http3Session::CloseStreamInternal(Http3StreamBase* aStream,
                                       nsresult aResult) {
  LOG3(("Http3Session::CloseStreamInternal %p %p 0x%" PRIx32, this, aStream,
        static_cast<uint32_t>(aResult)));
  if (aStream->HasStreamId()) {
    // We know the transaction reusing an idle connection has succeeded or
    // failed.
    if (mFirstStreamIdReuseIdleConnection.isSome() &&
        aStream->StreamId() == *mFirstStreamIdReuseIdleConnection) {
      MOZ_ASSERT(mConnectionIdleStart);
      MOZ_ASSERT(mConnectionIdleEnd);

#ifndef ANDROID
      if (mConnectionIdleStart) {
        mozilla::glean::netwerk::http3_time_to_reuse_idle_connection
            .Get(NS_SUCCEEDED(aResult) ? "succeeded"_ns : "failed"_ns)
            .AccumulateRawDuration(mConnectionIdleEnd - mConnectionIdleStart);
      }
#endif

      mConnectionIdleStart = TimeStamp();
      mConnectionIdleEnd = TimeStamp();
      mFirstStreamIdReuseIdleConnection.reset();
    }

    mStreamIdHash.Remove(aStream->StreamId());

    // Start to idle when we remove the last stream.
    if (mStreamIdHash.IsEmpty()) {
      mConnectionIdleStart = TimeStamp::Now();
    }
  }
  RemoveStreamFromQueues(aStream);
  if (nsAHttpTransaction* transaction = aStream->Transaction()) {
    mStreamTransactionHash.Remove(transaction);
  }
  mWebTransportSessions.RemoveElement(aStream);
  mWebTransportStreams.RemoveElement(aStream);
  mTunnelStreams.RemoveElement(aStream);
  // Close(NS_OK) implies that the NeqoHttp3Conn will be closed, so we can only
  // do this when there is no Http3Steeam, WebTransportSession and
  // WebTransportStream.
  if ((mShouldClose || mGoawayReceived) && HasNoActiveStreams()) {
    MOZ_ASSERT(!IsClosing());
    Close(NS_OK);
  }
}

void Http3Session::CloseWebTransportStream(Http3WebTransportStream* aStream,
                                           nsresult aResult) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG3(("Http3Session::CloseWebTransportStream %p %p 0x%" PRIx32, this, aStream,
        static_cast<uint32_t>(aResult)));
  if (aStream && !aStream->RecvdFin() && !aStream->RecvdReset() &&
      (aStream->HasStreamId())) {
    mHttp3Connection->ResetStream(aStream->StreamId(),
                                  HTTP3_APP_ERROR_REQUEST_CANCELLED);
  }

  aStream->Close(aResult);
  CloseStreamInternal(aStream, aResult);
}

void Http3Session::ResetWebTransportStream(Http3WebTransportStream* aStream,
                                           uint64_t aErrorCode) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG3(("Http3Session::ResetWebTransportStream %p %p 0x%" PRIx64, this, aStream,
        aErrorCode));
  mHttp3Connection->ResetStream(aStream->StreamId(), aErrorCode);
}

void Http3Session::StreamStopSending(Http3WebTransportStream* aStream,
                                     uint8_t aErrorCode) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG(("Http3Session::StreamStopSending %p %p 0x%" PRIx32, this, aStream,
       static_cast<uint32_t>(aErrorCode)));
  mHttp3Connection->StreamStopSending(aStream->StreamId(), aErrorCode);
}

nsresult Http3Session::TakeTransport(nsISocketTransport**,
                                     nsIAsyncInputStream**,
                                     nsIAsyncOutputStream**) {
  MOZ_ASSERT(false, "TakeTransport of Http3Session");
  return NS_ERROR_UNEXPECTED;
}

WebTransportSessionBase* Http3Session::GetWebTransportSession(
    nsAHttpTransaction* aTransaction) {
  RefPtr<Http3StreamBase> stream = mStreamTransactionHash.Get(aTransaction);

  if (!stream || !stream->GetHttp3WebTransportSession()) {
    MOZ_ASSERT(false, "There must be a stream");
    return nullptr;
  }
  RemoveStreamFromQueues(stream);
  mStreamTransactionHash.Remove(aTransaction);
  mWebTransportSessions.AppendElement(stream);
  return stream->GetHttp3WebTransportSession();
}

bool Http3Session::IsPersistent() { return true; }

void Http3Session::DontReuse() {
  LOG3(("Http3Session::DontReuse %p\n", this));
  if (!OnSocketThread()) {
    LOG3(("Http3Session %p not on socket thread\n", this));
    nsCOMPtr<nsIRunnable> event = NewRunnableMethod(
        "Http3Session::DontReuse", this, &Http3Session::DontReuse);
    if (StaticPrefs::network_trr_high_priority_events() &&
        mConnInfo->GetIsTrrServiceChannel()) {
      event = new PrioritizableRunnable(
          event.forget(), nsIRunnablePriority::PRIORITY_MEDIUMHIGH);
    }
    gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL);
    return;
  }

  if (mGoawayReceived || IsClosing()) {
    return;
  }

  mShouldClose = true;
  if (HasNoActiveStreams()) {
    // This is a temporary workaround and should be fixed properly in Happy
    // Eyeballs project. We should not exclude this domain if
    // Http3Session::DontReuse is called from
    // ConnectionEntry::MakeAllDontReuseExcept.
    if (mUdpConn &&
        mUdpConn->CloseReason() ==
            ConnectionCloseReason::CLOSE_EXISTING_CONN_FOR_COALESCING) {
      mDontExclude = true;
    }
    Close(NS_OK);
  }
}

void Http3Session::CloseWebTransportConn() {
  LOG3(("Http3Session::CloseWebTransportConn %p\n", this));
  // We need to dispatch, since Http3Session could be released in
  // HttpConnectionUDP::CloseTransaction.
  nsCOMPtr<nsIRunnable> event = NS_NewRunnableFunction(
      "Http3Session::CloseWebTransportConn", [self = RefPtr{this}]() {
        if (self->mUdpConn) {
          self->mUdpConn->CloseTransaction(self, NS_ERROR_ABORT);
        }
      });
  if (StaticPrefs::network_trr_high_priority_events() &&
      mConnInfo->GetIsTrrServiceChannel()) {
    event = new PrioritizableRunnable(event.forget(),
                                      nsIRunnablePriority::PRIORITY_MEDIUMHIGH);
  }
  gSocketTransportService->Dispatch(event.forget(), NS_DISPATCH_NORMAL);
}

void Http3Session::CurrentBrowserIdChanged(uint64_t id) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  mCurrentBrowserId = id;

  for (const auto& stream : mStreamTransactionHash.Values()) {
    RefPtr<Http3Stream> httpStream = stream->GetHttp3Stream();
    if (httpStream) {
      httpStream->CurrentBrowserIdChanged(id);
    }
  }
}

// This is called by Http3Stream::OnWriteSegment.
nsresult Http3Session::ReadResponseData(uint64_t aStreamId, char* aBuf,
                                        uint32_t aCount,
                                        uint32_t* aCountWritten, bool* aFin) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  nsresult rv = mHttp3Connection->ReadResponseData(aStreamId, (uint8_t*)aBuf,
                                                   aCount, aCountWritten, aFin);

  // This should not happen, i.e. stream must be present in neqo and in necko at
  // the same time.
  MOZ_ASSERT(rv != NS_ERROR_INVALID_ARG);
  if (NS_FAILED(rv)) {
    LOG3(("Http3Session::ReadResponseData return an error %" PRIx32
          " [this=%p]",
          static_cast<uint32_t>(rv), this));
    // This error will be handled by neqo and the whole connection will be
    // closed. We will return NS_BASE_STREAM_WOULD_BLOCK here.
    *aCountWritten = 0;
    *aFin = false;
    rv = NS_BASE_STREAM_WOULD_BLOCK;
  }

  MOZ_ASSERT((*aCountWritten != 0) || aFin || NS_FAILED(rv));
  return rv;
}

void Http3Session::TransactionHasDataToWrite(nsAHttpTransaction* caller) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG3(("Http3Session::TransactionHasDataToWrite %p trans=%p", this, caller));

  // a trapped signal from the http transaction to the connection that
  // it is no longer blocked on read.

  RefPtr<Http3StreamBase> stream = mStreamTransactionHash.Get(caller);
  if (!stream) {
    LOG3(("Http3Session::TransactionHasDataToWrite %p caller %p not found",
          this, caller));
    return;
  }

  LOG3(("Http3Session::TransactionHasDataToWrite %p ID is 0x%" PRIx64, this,
        stream->StreamId()));

  StreamHasDataToWrite(stream);
}

void Http3Session::StreamHasDataToWrite(Http3StreamBase* aStream) {
  if (!IsClosing()) {
    StreamReadyToWrite(aStream);
  } else {
    LOG3(
        ("Http3Session::TransactionHasDataToWrite %p closed so not setting "
         "Ready4Write\n",
         this));
  }

  // NSPR poll will not poll the network if there are non system PR_FileDesc's
  // that are ready - so we can get into a deadlock waiting for the system IO
  // to come back here if we don't force the send loop manually.
  (void)ForceSend();
}

void Http3Session::TransactionHasDataToRecv(nsAHttpTransaction* caller) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG3(("Http3Session::TransactionHasDataToRecv %p trans=%p", this, caller));

  // a signal from the http transaction to the connection that it will consume
  // more
  RefPtr<Http3StreamBase> stream = mStreamTransactionHash.Get(caller);
  if (!stream) {
    LOG3(("Http3Session::TransactionHasDataToRecv %p caller %p not found", this,
          caller));
    return;
  }

  LOG3(("Http3Session::TransactionHasDataToRecv %p ID is 0x%" PRIx64 "\n", this,
        stream->StreamId()));
  ConnectSlowConsumer(stream);
}

void Http3Session::ConnectSlowConsumer(Http3StreamBase* stream) {
  LOG3(("Http3Session::ConnectSlowConsumer %p 0x%" PRIx64 "\n", this,
        stream->StreamId()));
  mSlowConsumersReadyForRead.AppendElement(stream);
  (void)ForceRecv();
}

bool Http3Session::TestJoinConnection(const nsACString& hostname,
                                      int32_t port) {
  return RealJoinConnection(hostname, port, true);
}

bool Http3Session::JoinConnection(const nsACString& hostname, int32_t port) {
  return RealJoinConnection(hostname, port, false);
}

// TODO test
bool Http3Session::RealJoinConnection(const nsACString& hostname, int32_t port,
                                      bool justKidding) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  if (!mConnection || !CanSendData() || mShouldClose || mGoawayReceived) {
    return false;
  }

  nsHttpConnectionInfo* ci = ConnectionInfo();
  if (ci->UsingProxy()) {
    MOZ_ASSERT(false,
               "RealJoinConnection should not be called when using proxy");
    return false;
  }

  if (nsCString(hostname).EqualsIgnoreCase(ci->Origin()) &&
      (port == ci->OriginPort())) {
    return true;
  }

  nsAutoCString key(hostname);
  key.Append(':');
  key.Append(justKidding ? 'k' : '.');
  key.AppendInt(port);
  bool cachedResult;
  if (mJoinConnectionCache.Get(key, &cachedResult)) {
    LOG(("joinconnection [%p %s] %s result=%d cache\n", this,
         ConnectionInfo()->HashKey().get(), key.get(), cachedResult));
    return cachedResult;
  }

  nsresult rv;
  bool isJoined = false;

  nsCOMPtr<nsITLSSocketControl> sslSocketControl;
  mConnection->GetTLSSocketControl(getter_AddRefs(sslSocketControl));
  if (!sslSocketControl) {
    return false;
  }

  bool joinedReturn = false;
  if (justKidding) {
    rv = sslSocketControl->TestJoinConnection(mConnInfo->GetNPNToken(),
                                              hostname, port, &isJoined);
  } else {
    rv = sslSocketControl->JoinConnection(mConnInfo->GetNPNToken(), hostname,
                                          port, &isJoined);
  }
  if (NS_SUCCEEDED(rv) && isJoined) {
    joinedReturn = true;
  }

  LOG(("joinconnection [%p %s] %s result=%d lookup\n", this,
       ConnectionInfo()->HashKey().get(), key.get(), joinedReturn));
  mJoinConnectionCache.InsertOrUpdate(key, joinedReturn);
  if (!justKidding) {
    // cache a kidding entry too as this one is good for both
    nsAutoCString key2(hostname);
    key2.Append(':');
    key2.Append('k');
    key2.AppendInt(port);
    if (!mJoinConnectionCache.Get(key2)) {
      mJoinConnectionCache.InsertOrUpdate(key2, joinedReturn);
    }
  }
  return joinedReturn;
}

void Http3Session::CallCertVerification(Maybe<nsCString> aEchPublicName) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  LOG(("Http3Session::CallCertVerification [this=%p]", this));

  NeqoCertificateInfo certInfo;
  if (NS_FAILED(mHttp3Connection->PeerCertificateInfo(&certInfo))) {
    LOG(("Http3Session::CallCertVerification [this=%p] - no cert", this));
    mHttp3Connection->PeerAuthenticated(SSL_ERROR_BAD_CERTIFICATE);
    mError = psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERTIFICATE);
    return;
  }

  if (mConnInfo->GetWebTransport()) {
    // if our connection is webtransport, we might do a verification
    // based on serverCertificatedHashes
    const nsTArray<RefPtr<nsIWebTransportHash>>* servCertHashes =
        gHttpHandler->ConnMgr()->GetServerCertHashes(mConnInfo);
    if (servCertHashes && !servCertHashes->IsEmpty() &&
        certInfo.certs.Length() >= 1) {
      // ok, we verify based on serverCertificateHashes
      mozilla::pkix::Result rv = AuthCertificateWithServerCertificateHashes(
          certInfo.certs[0], *servCertHashes);
      if (rv != mozilla::pkix::Result::Success) {
        // ok we failed, report it back
        LOG(
            ("Http3Session::CallCertVerification [this=%p] "
             "AuthCertificateWithServerCertificateHashes failed",
             this));
        mHttp3Connection->PeerAuthenticated(SSL_ERROR_BAD_CERTIFICATE);
        mError = psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERTIFICATE);
        return;
      }
      // ok, we succeded
      Authenticated(0, true);
      return;
    }
  }

  Maybe<nsTArray<nsTArray<uint8_t>>> stapledOCSPResponse;
  if (certInfo.stapled_ocsp_responses_present) {
    stapledOCSPResponse.emplace(std::move(certInfo.stapled_ocsp_responses));
  }

  Maybe<nsTArray<uint8_t>> sctsFromTLSExtension;
  if (certInfo.signed_cert_timestamp_present) {
    sctsFromTLSExtension.emplace(std::move(certInfo.signed_cert_timestamp));
  }

  uint32_t providerFlags;
  // the return value is always NS_OK, just ignore it.
  (void)mSocketControl->GetProviderFlags(&providerFlags);

  nsCString echConfig;
  nsresult nsrv = mSocketControl->GetEchConfig(echConfig);
  bool verifyToEchPublicName = NS_SUCCEEDED(nsrv) && !echConfig.IsEmpty() &&
                               aEchPublicName && !aEchPublicName->IsEmpty();
  const nsACString& hostname =
      verifyToEchPublicName ? *aEchPublicName : mSocketControl->GetHostName();

  SECStatus rv = psm::AuthCertificateHookWithInfo(
      mSocketControl, hostname, static_cast<const void*>(this),
      std::move(certInfo.certs), stapledOCSPResponse, sctsFromTLSExtension,
      providerFlags);
  if ((rv != SECSuccess) && (rv != SECWouldBlock)) {
    LOG(("Http3Session::CallCertVerification [this=%p] AuthCertificate failed",
         this));
    mHttp3Connection->PeerAuthenticated(SSL_ERROR_BAD_CERTIFICATE);
    mError = psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERTIFICATE);
  }
}

void Http3Session::Authenticated(int32_t aError,
                                 bool aServCertHashesSucceeded) {
  LOG(("Http3Session::Authenticated error=0x%" PRIx32 " [this=%p].", aError,
       this));
  if ((mState == INITIALIZING) || (mState == ZERORTT)) {
    if (psm::IsNSSErrorCode(aError)) {
      mError = psm::GetXPCOMFromNSSError(aError);
      LOG(("Http3Session::Authenticated psm-error=0x%" PRIx32 " [this=%p].",
           static_cast<uint32_t>(mError), this));
    } else if (StaticPrefs::
                   network_http_http3_disable_when_third_party_roots_found()) {
      // In test, we use another perf value to override the value of
      // hasThirdPartyRoots.
      bool hasThirdPartyRoots =
          (xpc::IsInAutomation() || PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR"))
              ? StaticPrefs::
                    network_http_http3_has_third_party_roots_found_in_automation()
              : !mSocketControl->IsBuiltCertChainRootBuiltInRoot();
      LOG(
          ("Http3Session::Authenticated [this=%p, hasThirdPartyRoots=%d, "
           "servCertHashesSucceeded=%d]",
           this, hasThirdPartyRoots, aServCertHashesSucceeded));
      // If serverCertificateHashes is used a thirdPartyRoot is legal
      if (hasThirdPartyRoots && !aServCertHashesSucceeded) {
        if (mFirstHttpTransaction) {
          mFirstHttpTransaction->DisableHttp3(false);
        }
        mUdpConn->CloseTransaction(this, NS_ERROR_NET_RESET);
        return;
      }
    }
    mHttp3Connection->PeerAuthenticated(aError);

    // Call OnQuicTimeoutExpired to properly process neqo events and outputs.
    // We call OnQuicTimeoutExpired instead of ProcessOutputAndEvents, because
    // HttpConnectionUDP must close this session in case of an error.
    nsCOMPtr<nsIRunnable> event =
        NewRunnableMethod("net::HttpConnectionUDP::OnQuicTimeoutExpired",
                          mUdpConn, &HttpConnectionUDP::OnQuicTimeoutExpired);
    if (StaticPrefs::network_trr_high_priority_events() &&
        mConnInfo->GetIsTrrServiceChannel()) {
      event = new PrioritizableRunnable(
          event.forget(), nsIRunnablePriority::PRIORITY_MEDIUMHIGH);
    }
    NS_DispatchToCurrentThread(event);
    mUdpConn->ChangeConnectionState(ConnectionState::TRANSFERING);
  }
}

void Http3Session::SetSecInfo() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  NeqoSecretInfo secInfo;
  if (NS_SUCCEEDED(mHttp3Connection->GetSecInfo(&secInfo))) {
    mSocketControl->SetSSLVersionUsed(secInfo.version);
    mSocketControl->SetResumed(secInfo.resumed);
    mSocketControl->SetNegotiatedNPN(secInfo.alpn);

    mSocketControl->SetInfo(secInfo.cipher, secInfo.version, secInfo.group,
                            secInfo.signature_scheme, secInfo.ech_accepted);
    mHandshakeSucceeded = true;
  }

  if (!mSocketControl->HasServerCert()) {
    mSocketControl->RebuildCertificateInfoFromSSLTokenCache();
  }
}

// Transport error have values from 0x0 to 0x11.
// (https://tools.ietf.org/html/draft-ietf-quic-transport-34#section-20.1)
// We will map this error to 0-16.
// 17 will capture error codes between and including 0x12 and 0x0ff. This
// error codes are not define by the spec but who know peer may sent them.
// CryptoAlerts have value 0x100 + alert code. The range of alert code is
// 0x00-0xff. (https://tools.ietf.org/html/draft-ietf-quic-tls_34#section-4.8)
// Since telemetry does not allow more than 100 bucket, we use three diffrent
// keys to map all alert codes.
const uint32_t HTTP3_TELEMETRY_TRANSPORT_INTERNAL_ERROR = 15;
const uint32_t HTTP3_TELEMETRY_TRANSPORT_END = 16;
const uint32_t HTTP3_TELEMETRY_TRANSPORT_UNKNOWN = 17;
const uint32_t HTTP3_TELEMETRY_TRANSPORT_CRYPTO_UNKNOWN = 18;
// All errors from CloseError::Tag::CryptoError will be map to 19
const uint32_t HTTP3_TELEMETRY_CRYPTO_ERROR = 19;

uint64_t GetCryptoAlertCode(nsCString& key, uint64_t error) {
  if (error < 100) {
    key.Append("_a"_ns);
    return error;
  }
  if (error < 200) {
    error -= 100;
    key.Append("_b"_ns);
    return error;
  }
  if (error < 256) {
    error -= 200;
    key.Append("_c"_ns);
    return error;
  }
  return HTTP3_TELEMETRY_TRANSPORT_CRYPTO_UNKNOWN;
}

uint64_t GetTransportErrorCodeForTelemetry(nsCString& key, uint64_t error) {
  if (error <= HTTP3_TELEMETRY_TRANSPORT_END) {
    return error;
  }
  if (error < 0x100) {
    return HTTP3_TELEMETRY_TRANSPORT_UNKNOWN;
  }

  return GetCryptoAlertCode(key, error - 0x100);
}

// Http3 error codes are 0x100-0x110.
// (https://tools.ietf.org/html/draft-ietf-quic-http-33#section-8.1)
// The mapping is described below.
// 0x00-0x10 mapped to 0-16
// 0x11-0xff mapped to 17
// 0x100-0x110 mapped to 18-36
// 0x111-0x1ff mapped to 37
// 0x200-0x202 mapped to 38-40
// Others mapped to 41
const uint32_t HTTP3_TELEMETRY_APP_UNKNOWN_1 = 17;
const uint32_t HTTP3_TELEMETRY_APP_START = 18;
// Values between 0x111 and 0x1ff are no definded and will be map to 18.
const uint32_t HTTP3_TELEMETRY_APP_UNKNOWN_2 = 37;
// Error codes between 0x200 and 0x202 are related to qpack.
// (https://tools.ietf.org/html/draft-ietf-quic-qpack-20#section-6)
// They will be mapped to 19-21
const uint32_t HTTP3_TELEMETRY_APP_QPACK_START = 38;
// Values greater or equal to 0x203 are no definded and will be map to 41.
const uint32_t HTTP3_TELEMETRY_APP_UNKNOWN_3 = 41;

uint64_t GetAppErrorCodeForTelemetry(uint64_t error) {
  if (error <= 0x10) {
    return error;
  }
  if (error <= 0xff) {
    return HTTP3_TELEMETRY_APP_UNKNOWN_1;
  }
  if (error <= 0x110) {
    return error - 0x100 + HTTP3_TELEMETRY_APP_START;
  }
  if (error < 0x200) {
    return HTTP3_TELEMETRY_APP_UNKNOWN_2;
  }
  if (error <= 0x202) {
    return error - 0x200 + HTTP3_TELEMETRY_APP_QPACK_START;
  }
  return HTTP3_TELEMETRY_APP_UNKNOWN_3;
}

void Http3Session::CloseConnectionTelemetry(CloseError& aError, bool aClosing) {
  uint64_t value = 0;
  nsCString key = EmptyCString();

  switch (aError.tag) {
    case CloseError::Tag::TransportInternalError:
      key = "transport_internal"_ns;
      value = HTTP3_TELEMETRY_TRANSPORT_INTERNAL_ERROR;
      break;
    case CloseError::Tag::TransportInternalErrorOther:
      key = "transport_other"_ns;
      value = aError.transport_internal_error_other._0;
      break;
    case CloseError::Tag::TransportError:
      key = "transport"_ns;
      value = GetTransportErrorCodeForTelemetry(key, aError.transport_error._0);
      break;
    case CloseError::Tag::CryptoError:
      key = "transport"_ns;
      value = HTTP3_TELEMETRY_CRYPTO_ERROR;
      break;
    case CloseError::Tag::CryptoAlert:
      key = "transport_crypto_alert"_ns;
      value = GetCryptoAlertCode(key, aError.crypto_alert._0);
      break;
    case CloseError::Tag::PeerAppError:
      key = "peer_app"_ns;
      value = GetAppErrorCodeForTelemetry(aError.peer_app_error._0);
      break;
    case CloseError::Tag::PeerError:
      key = "peer_transport"_ns;
      value = GetTransportErrorCodeForTelemetry(key, aError.peer_error._0);
      break;
    case CloseError::Tag::AppError:
      key = "app"_ns;
      value = GetAppErrorCodeForTelemetry(aError.app_error._0);
      break;
    case CloseError::Tag::EchRetry:
      key = "transport_crypto_alert"_ns;
      value = 100;
  }

  MOZ_DIAGNOSTIC_ASSERT(value <= 100);

  key.Append(aClosing ? "_closing"_ns : "_closed"_ns);

  glean::http3::connection_close_code.Get(key).AccumulateSingleSample(value);

  Http3Stats stats{};
  mHttp3Connection->GetStats(&stats);

  if (stats.packets_tx > 0) {
    unsigned long loss = (stats.lost * 10000) / stats.packets_tx;
    glean::http3::loss_ratio.AccumulateSingleSample(loss);

    glean::http3::late_ack.EnumGet(glean::http3::LateAckLabel::eAck)
        .AccumulateSingleSample(stats.late_ack);
    glean::http3::late_ack.EnumGet(glean::http3::LateAckLabel::ePto)
        .AccumulateSingleSample(stats.pto_ack);

    unsigned long late_ack_ratio = (stats.late_ack * 10000) / stats.packets_tx;
    unsigned long pto_ack_ratio = (stats.pto_ack * 10000) / stats.packets_tx;
    glean::http3::late_ack_ratio.EnumGet(glean::http3::LateAckRatioLabel::eAck)
        .AccumulateSingleSample(late_ack_ratio);
    glean::http3::late_ack_ratio.EnumGet(glean::http3::LateAckRatioLabel::ePto)
        .AccumulateSingleSample(pto_ack_ratio);

    for (uint32_t i = 0; i < MAX_PTO_COUNTS; i++) {
      nsAutoCString key;
      key.AppendInt(i);
      glean::http3::counts_pto.Get(key).AccumulateSingleSample(
          stats.pto_counts[i]);
    }

    glean::http3::drop_dgrams.AccumulateSingleSample(stats.dropped_rx);
    glean::http3::saved_dgrams.AccumulateSingleSample(stats.saved_datagrams);
  }

  glean::http3::received_sent_dgrams
      .EnumGet(glean::http3::ReceivedSentDgramsLabel::eReceived)
      .AccumulateSingleSample(stats.packets_rx);
  glean::http3::received_sent_dgrams
      .EnumGet(glean::http3::ReceivedSentDgramsLabel::eSent)
      .AccumulateSingleSample(stats.packets_tx);

  if (aClosing) {
    RefPtr<nsHttpConnectionInfo> ci;
    GetConnectionInfo(getter_AddRefs(ci));
    if (ci && ci->GetIsTrrServiceChannel() && !mLastTRRResponseTime.IsNull() &&
        (mGoawayReceived ||
         (aError.tag == CloseError::Tag::PeerAppError &&
          aError.peer_app_error._0 == HTTP3_APP_ERROR_NO_ERROR))) {
      // Record telemetry keyed by TRR provider.
      glean::network::trr_idle_close_time_h3.Get(TRRProviderKey())
          .AccumulateRawDuration(TimeStamp::Now() - mLastTRRResponseTime);
      mLastTRRResponseTime = TimeStamp();
    }
  }
}

void Http3Session::Finish0Rtt(bool aRestart) {
  RefPtr<Http3Session> self(this);

  nsTArray<RefPtr<Http3StreamBase>> streams;
  for (const auto& weak : m0RTTStreams) {
    if (RefPtr<Http3StreamBase> s = weak.get()) {
      streams.AppendElement(std::move(s));
    }
  }
  m0RTTStreams.Clear();

  for (const auto& stream : streams) {
    if (aRestart) {
      // When we need to restart transactions remove them from all lists.
      if (stream->HasStreamId()) {
        mStreamIdHash.Remove(stream->StreamId());
      }
      RemoveStreamFromQueues(stream);
      // The stream is ready to write again.
      mReadyForWrite.Push(stream);
    }
    stream->Finish0RTT(aRestart);
  }

  for (size_t i = 0; i < mCannotDo0RTTStreams.Length(); ++i) {
    if (mCannotDo0RTTStreams[i]) {
      mReadyForWrite.Push(mCannotDo0RTTStreams[i]);
    }
  }
  mCannotDo0RTTStreams.Clear();
  MaybeResumeSend();
}

void Http3Session::ReportHttp3Connection() {
  if (CanSendData() && !mHttp3ConnectionReported) {
    mHttp3ConnectionReported = true;
    gHttpHandler->ConnMgr()->ReportHttp3Connection(mUdpConn);
    MaybeResumeSend();
  }
}

#ifndef ANDROID
void Http3Session::EchOutcomeTelemetry() {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");

  glean::http3::EchOutcomeLabel label;
  switch (mEchExtensionStatus) {
    case EchExtensionStatus::kNotPresent:
      label = glean::http3::EchOutcomeLabel::eNone;
      break;
    case EchExtensionStatus::kGREASE:
      label = glean::http3::EchOutcomeLabel::eGrease;
      break;
    case EchExtensionStatus::kReal:
      label = glean::http3::EchOutcomeLabel::eReal;
      break;
  }

  glean::http3::ech_outcome.EnumGet(label).AccumulateSingleSample(
      mHandshakeSucceeded ? 0 : 1);
}
#endif

void Http3Session::ZeroRttTelemetry(ZeroRttOutcome aOutcome) {
  nsAutoCString key;

  switch (aOutcome) {
    case USED_SUCCEEDED:
      key = "succeeded"_ns;
      break;
    case USED_REJECTED:
      key = "rejected"_ns;
      break;
    case USED_CONN_ERROR:
      key = "conn_error"_ns;
      break;
    case USED_CONN_CLOSED_BY_NECKO:
      key = "conn_closed_by_necko"_ns;
      break;
    default:
      break;
  }

  bool isTrr = mConnInfo && mConnInfo->GetIsTrrServiceChannel();

  if (key.IsEmpty()) {
    mozilla::glean::netwerk::http3_0rtt_state.Get("not_used"_ns).Add(1);
    if (isTrr) {
      mozilla::glean::dns::trr_http3_0rtt_state.Get("not_used"_ns).Add(1);
    }
  } else {
    MOZ_ASSERT(mZeroRttStarted);
    mozilla::TimeStamp zeroRttEnded = mozilla::TimeStamp::Now();
    mozilla::glean::netwerk::http3_0rtt_state_duration.Get(key)
        .AccumulateRawDuration(zeroRttEnded - mZeroRttStarted);
    mozilla::glean::netwerk::http3_0rtt_state.Get(key).Add(1);

    if (isTrr) {
      mozilla::glean::dns::trr_http3_0rtt_state_duration.Get(key)
          .AccumulateRawDuration(zeroRttEnded - mZeroRttStarted);
      mozilla::glean::dns::trr_http3_0rtt_state.Get(key).Add(1);
    }
  }
}

nsresult Http3Session::GetTransactionTLSSocketControl(
    nsITLSSocketControl** tlsSocketControl) {
  NS_IF_ADDREF(*tlsSocketControl = mSocketControl);
  return NS_OK;
}

PRIntervalTime Http3Session::LastWriteTime() { return mLastWriteTime; }

//=========================================================================
// WebTransport
//=========================================================================

nsresult Http3Session::CloseWebTransport(uint64_t aSessionId, uint32_t aError,
                                         const nsACString& aMessage) {
  return mHttp3Connection->CloseWebTransport(aSessionId, aError, aMessage);
}

nsresult Http3Session::CreateWebTransportStream(
    uint64_t aSessionId, WebTransportStreamType aStreamType,
    uint64_t* aStreamId) {
  return mHttp3Connection->CreateWebTransportStream(aSessionId, aStreamType,
                                                    aStreamId);
}

void Http3Session::SendDatagram(Http3WebTransportSession* aSession,
                                nsTArray<uint8_t>& aData,
                                uint64_t aTrackingId) {
  nsresult rv = mHttp3Connection->WebTransportSendDatagram(aSession->StreamId(),
                                                           aData, aTrackingId);
  LOG(("Http3Session::SendDatagram %p res=%" PRIx32, this,
       static_cast<uint32_t>(rv)));
  if (!aTrackingId) {
    return;
  }

  switch (rv) {
    case NS_OK:
      aSession->OnOutgoingDatagramOutCome(
          aTrackingId, WebTransportSessionEventListener::DatagramOutcome::SENT);
      break;
    case NS_ERROR_NOT_AVAILABLE:
      aSession->OnOutgoingDatagramOutCome(
          aTrackingId, WebTransportSessionEventListener::DatagramOutcome::
                           DROPPED_TOO_MUCH_DATA);
      break;
    default:
      aSession->OnOutgoingDatagramOutCome(
          aTrackingId,
          WebTransportSessionEventListener::DatagramOutcome::UNKNOWN);
      break;
  }
}

uint64_t Http3Session::MaxDatagramSize(uint64_t aSessionId) {
  uint64_t size = 0;
  (void)mHttp3Connection->WebTransportMaxDatagramSize(aSessionId, &size);
  return size;
}

void Http3Session::SendHTTPDatagram(uint64_t aStreamId,
                                    nsTArray<uint8_t>& aData,
                                    uint64_t aTrackingId) {
  LOG(("Http3Session::SendHTTPDatagram %p length=%zu aTrackingId=%" PRIx64,
       this, aData.Length(), aTrackingId));
  (void)mHttp3Connection->ConnectUdpSendDatagram(aStreamId, aData, aTrackingId);
}

void Http3Session::SetSendOrder(Http3StreamBase* aStream,
                                Maybe<int64_t> aSendOrder) {
  if (!IsClosing()) {
    nsresult rv = mHttp3Connection->WebTransportSetSendOrder(
        aStream->StreamId(), aSendOrder);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    (void)rv;
  }
}

Http3Stats Http3Session::GetStats() {
  if (!mHttp3Connection) {
    return Http3Stats();
  }

  Http3Stats stats{};
  mHttp3Connection->GetStats(&stats);
  return stats;
}

already_AddRefed<HttpConnectionUDP> Http3Session::CreateTunnelStream(
    nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks) {
  LOG(("Http3Session::CreateTunnelStream %p aHttpTransaction=%p", this,
       aHttpTransaction));
  RefPtr<Http3StreamBase> stream =
      new Http3ConnectUDPStream(aHttpTransaction, this, NS_GetCurrentThread());
  if (mConnInfo->GetIsTrrServiceChannel()) {
    stream->GetHttp3ConnectUDPStream()->MarkAsTRRServiceChannel();
  }
  mStreamTransactionHash.InsertOrUpdate(aHttpTransaction, RefPtr{stream});
  StreamHasDataToWrite(stream);

  RefPtr<HttpConnectionUDP> conn =
      stream->GetHttp3ConnectUDPStream()->CreateUDPConnection(aCallbacks);
  return conn.forget();
}

void Http3Session::FinishTunnelSetup(nsAHttpTransaction* aTransaction) {
  LOG(("Http3Session::FinishTunnelSetup %p aHttpTransaction=%p", this,
       aTransaction));
  RefPtr<Http3StreamBase> stream = mStreamTransactionHash.Get(aTransaction);
  if (!stream || !stream->GetHttp3ConnectUDPStream()) {
    MOZ_ASSERT(false, "There must be a stream");
    return;
  }

  RemoveStreamFromQueues(stream);
  mStreamTransactionHash.Remove(aTransaction);
  mTunnelStreams.AppendElement(stream);
}

already_AddRefed<nsHttpConnection> Http3Session::CreateTunnelStream(
    nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks,
    PRIntervalTime aRtt, bool aIsExtendedCONNECT) {
  LOG(("Http3Session::CreateTunnelStream %p aHttpTransaction=%p", this,
       aHttpTransaction));
  RefPtr<Http3StreamBase> stream =
      new Http3StreamTunnel(aHttpTransaction, this, mCurrentBrowserId);
  mStreamTransactionHash.InsertOrUpdate(aHttpTransaction, RefPtr{stream});
  StreamHasDataToWrite(stream);

  RefPtr<nsHttpConnection> conn =
      stream->GetHttp3StreamTunnel()->CreateHttpConnection(aCallbacks, aRtt,
                                                           aIsExtendedCONNECT);
  return conn.forget();
}

}  // namespace mozilla::net
