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

//go:build go1.27 && !http2legacy

// Transport wrapping a net/http.Transport.

package http2

import (
	"context"
	"crypto/tls"
	"errors"
	"math"
	"net"
	"net/http"
	"net/http/httptrace"
	"slices"
	"sync"
	"time"
)

func configureTransport(t1 *http.Transport) error {
	_, err := configureTransports(t1)
	return err
}

func configureTransports(t1 *http.Transport) (*Transport, error) {
	// ConfigureTransport returns an http2.Transport with a configuration
	// linked to the http.Transport's.
	tr2 := &Transport{}
	tr2.configure(t1)
	// Enable HTTP/2 on the transport, as the pre-wrapping implementation did:
	// net/http does not auto-enable it for a transport with a custom
	// TLSClientConfig or dialer.
	if t1.TLSClientConfig == nil {
		t1.TLSClientConfig = &tls.Config{}
	}
	if t1.Protocols == nil {
		t1.Protocols = new(http.Protocols)
		t1.Protocols.SetHTTP1(true)
	}
	t1.Protocols.SetHTTP2(true)
	return tr2, nil
}

// transportConfig is passed to net/http.Transport.RegisterProtocol("http/2", config).
// It provides the net/http.Transport with access to the configuration in the
// x/net/http2.Transport.
type transportConfig struct {
	t *Transport
}

// Registered is called by net/http.Transport.RegisterProtocol,
// to let us know that it understands the registration mechanism we're using.
func (t transportConfig) Registered(t1 *http.Transport) {
	t.t.lazyt1 = t1
}

func (t transportConfig) DisableCompression() bool {
	return t.t.DisableCompression
}

func (t transportConfig) MaxHeaderListSize() int64 {
	return int64(t.t.MaxHeaderListSize)
}

func (t transportConfig) IdleConnTimeout() time.Duration {
	return t.t.IdleConnTimeout
}

func (t transportConfig) HTTP2Config() http.HTTP2Config {
	return http.HTTP2Config{
		StrictMaxConcurrentRequests: t.t.StrictMaxConcurrentStreams,
		MaxDecoderHeaderTableSize:   int(t.t.MaxDecoderHeaderTableSize),
		MaxEncoderHeaderTableSize:   int(t.t.MaxEncoderHeaderTableSize),
		MaxReadFrameSize:            int(t.t.MaxReadFrameSize),
		SendPingTimeout:             t.t.ReadIdleTimeout,
		PingTimeout:                 t.t.PingTimeout,
		WriteByteTimeout:            t.t.WriteByteTimeout,
		CountError:                  t.t.CountError,
	}
}

// ExternalRoundTrip reports whether the Transport wants to take control of the RoundTrip call.
// If the user hasn't configured a custom connection pool, we leave the RoundTrip up to net/http.
func (t transportConfig) ExternalRoundTrip() bool {
	return t.t.ConnPool != nil
}

// RoundTrip is used when the http.Transport is passing control of the full
// RoundTrip to us--connection pooling, retries, etc.
//
// This is only used when the http2.Transport has a user-provided ConnPool.
// Any other time, net/http handles everything.
func (t transportConfig) RoundTrip(req *http.Request) (*http.Response, error) {
	if t.t.ConnPool == nil {
		return nil, http.ErrSkipAltProtocol
	}
	return t.t.RoundTrip(req)
}

// netConnContextKey passes a net.Conn to http.Transport.NewClientConn.
// See http2.Transport.NewClientConn.
type netConnContextKey struct{}

// ConnFromContext lets the http.Transport fetch a net.Conn out of a context
// passed to NewClientConn. See http2.Transport.NewClientConn.
func (t transportConfig) ConnFromContext(ctx context.Context) net.Conn {
	nc, _ := ctx.Value(netConnContextKey{}).(net.Conn)
	return nc
}

// http2TransportContextKey marks a RoundTrip as needing its dial handled by the http2.Transport.
// We set this for http2.RoundTrip calls, where the historical behavior is to use the
// http2.Transport's dialer.
type http2TransportContextKey struct{}

// DialFromContext dials a new connection using the http2.Transport's DialTLS/DialTLSContext.
func (t transportConfig) DialFromContext(ctx context.Context, network, address string) (net.Conn, error) {
	if ctx.Value(http2TransportContextKey{}) == nil {
		// We're being called from a RoundTrip that did not start with an http2.Transport.
		// Use the http.Transport's dialer.
		return nil, errors.ErrUnsupported
	}

	tlsConf := t.t.TLSClientConfig
	if tlsConf == nil {
		tlsConf = &tls.Config{}
	} else {
		tlsConf = tlsConf.Clone()
	}
	if !slices.Contains(tlsConf.NextProtos, "h2") {
		tlsConf.NextProtos = append([]string{"h2"}, tlsConf.NextProtos...)
	}
	if tlsConf.ServerName == "" {
		host, _, err := net.SplitHostPort(address)
		if err == nil {
			tlsConf.ServerName = host
		}
	}
	return t.t.dialTLS(ctx, network, address, tlsConf)
}

type transportInternal struct {
	initOnce sync.Once
	lazyt1   *http.Transport
}

func (t *Transport) init() *http.Transport {
	t.initOnce.Do(func() {
		if t.lazyt1 != nil {
			return
		}
		t1 := &http.Transport{}
		t.configure(t1)
	})
	return t.lazyt1
}

func (t *Transport) configure(t1 *http.Transport) {
	t1.RegisterProtocol("http/2", transportConfig{t})
	// tr2.lazyt1 is set by transportConfig.Registered.
	if t.lazyt1 != t1 {
		panic("http2: net/http does not support this version of x/net/http2")
	}
}

func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
	t1 := t.init()

	if req.URL.Scheme == "http" && !t.AllowHTTP {
		return nil, errors.New("http2: unencrypted HTTP/2 not enabled")
	}

	// When the Transport has a user-provided connection pool (unusual, deprecated),
	// we need to handle picking a connection, retrys, etc.
	if t.ConnPool != nil {
		return t.roundTripViaPool(req, opt, t.ConnPool)
	}

	// Setting this context key lets net/http know that if it is necessary to dial
	// a new connection, we should handle the net.Dial.
	//
	// Both http.Transport and http2.Transport allow the user to provide a custom
	// dial function, and historically you only get the dial function from the
	// Transport you're calling RoundTrip on.
	ctx := context.WithValue(req.Context(), http2TransportContextKey{}, t)
	req = req.WithContext(ctx)

	return t1.RoundTrip(req)
}

func (t *Transport) closeIdleConnections() {
	t1 := t.init()
	t1.CloseIdleConnections()
}

func (t *Transport) newUserClientConn(c net.Conn) (*ClientConn, error) {
	t1 := t.init()
	// http.Transport's NewClientConn doesn't provide a supported way to create
	// a connection from a net.Conn. (This might be useful to add in the future?)
	// We're going to craftily sneak one in via the context key, with the
	// scheme of "http/2" telling NewClientConn to look for it.
	ctx := context.WithValue(context.Background(), netConnContextKey{}, c)

	nhcc, err := t1.NewClientConn(ctx, "http/2", "")
	if err != nil {
		return nil, err
	}
	cc := &ClientConn{cc: nhcc, tr: t, tconn: c}
	nhcc.SetStateHook(cc.stateHook)
	return cc, nil
}

// ClientConn is the state of a single HTTP/2 client connection to an
// HTTP/2 server.
type ClientConn struct {
	cc         *http.ClientConn
	tconn      net.Conn
	tr         *Transport
	doNotReuse bool

	mu            sync.Mutex
	closing       bool
	closed        bool
	roundTrips    int
	reserved      int
	starting      int
	pending       int
	maxConcurrent int
	lastIdle      time.Time
	shutdownc     chan struct{}

	atomicReused uint32 // whether conn is being reused; atomic
}

func (cc *ClientConn) roundTrip(req *http.Request) (*http.Response, error) {
	err := func() error {
		cc.mu.Lock()
		defer cc.mu.Unlock()
		if cc.doNotReuse {
			return errClientConnUnusable
		}
		cc.roundTrips++
		if cc.reserved > 0 {
			// We've already reserved a concurrency slot for this request.
			cc.reserved--
		} else if cc.cc.Reserve() != nil {
			// We don't seem to have an available concurrency slot,
			// so bump the pending count (requests waiting for a slot).
			cc.pending++
		}
		// ClientConn.Shutdown will not shut down the conn while
		// cc.starting > 0 or cc.cc.InFlight() > 0.
		//
		// The starting state covers the gap between us deciding to
		// start sending the request, and actually sending it.
		cc.starting++
		return nil
	}()
	if err != nil {
		return nil, err
	}
	resp, err := cc.cc.RoundTrip(req)
	cc.mu.Lock()
	cc.starting--
	if cc.pending > 0 {
		// A request completing frees up a concurrency slot for
		// a pending request to start.
		cc.pending--
	}
	cc.updateStateLocked()
	cc.mu.Unlock()
	return resp, err
}

func (cc *ClientConn) canTakeNewRequest() bool {
	return cc.cc.Available() > 0 && !cc.doNotReuse
}

func (cc *ClientConn) close() error {
	return cc.cc.Close()
}

func (cc *ClientConn) ping(ctx context.Context) error {
	// Ask net/http to ping its connection by sending a request with a method of ":ping".
	_, err := cc.cc.RoundTrip((&http.Request{
		Method: ":ping",
	}).WithContext(ctx))
	return err
}

func (cc *ClientConn) reserveNewRequest() bool {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	if cc.doNotReuse {
		return false
	}
	if err := cc.cc.Reserve(); err != nil {
		return false
	}
	cc.reserved++
	return true
}

func (cc *ClientConn) setDoNotReuse() {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	cc.doNotReuse = true
	cc.closing = true
}

func (cc *ClientConn) shutdown(ctx context.Context) error {
	cc.mu.Lock()
	inFlight := cc.cc.InFlight() + cc.starting
	if inFlight > 0 && cc.shutdownc == nil {
		cc.shutdownc = make(chan struct{})
	}
	shutdownc := cc.shutdownc
	cc.mu.Unlock()
	if shutdownc != nil {
		// Wait for in-flight requests to finish.
		select {
		case <-shutdownc:
		case <-ctx.Done():
			return ctx.Err()
		}
	}
	cc.cc.Close()
	return nil
}

func (cc *ClientConn) state() ClientConnState {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	cc.updateStateLocked()
	return ClientConnState{
		Closed:               cc.closed,
		Closing:              cc.closing,
		StreamsActive:        cc.cc.InFlight() - cc.reserved,
		StreamsReserved:      cc.reserved,
		StreamsPending:       cc.pending,
		MaxConcurrentStreams: uint32(min(int64(cc.maxConcurrent), math.MaxUint32)),
		LastIdle:             cc.lastIdle,
	}
}

// stateHook is the http.ClientConn's state hook.
func (cc *ClientConn) stateHook(*http.ClientConn) {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	cc.updateStateLocked()
}

func (cc *ClientConn) updateStateLocked() {
	if cc.cc.Err() != nil && !cc.closed {
		cc.closing = true
		cc.closed = true
		if cc.tr.ConnPool != nil {
			// Do the ConnPool update in another goroutine,
			// to avoid holding the conn mutex while it runs.
			go cc.tr.ConnPool.MarkDead(cc)
		}
	}
	if cc.cc.InFlight() == 0 && cc.roundTrips > 0 && cc.starting == 0 {
		cc.lastIdle = time.Now()
	}
	if !cc.closed {
		// This is slightly racy (a request could start or finish in between
		// the Available and InFlight calls), but the best we can do given that
		// the net/http ClientConn API doesn't expose the conn's max concurrency.
		cc.maxConcurrent = cc.cc.Available() + cc.cc.InFlight()
	}
	if cc.shutdownc != nil && cc.cc.InFlight()+cc.starting == 0 {
		close(cc.shutdownc)
		cc.shutdownc = nil
	}
}

func (cc *ClientConn) stopIdleTimer() {}

// traceGotConn is (when http2legacy is not enabled) only used for tracing
// connections acquired while using a user-provided ClientConnPool.
func traceGotConn(req *http.Request, cc *ClientConn, reused bool) {
	trace := httptrace.ContextClientTrace(req.Context())
	if trace == nil || trace.GotConn == nil {
		return
	}
	ci := httptrace.GotConnInfo{Conn: cc.tconn}
	ci.Reused = reused
	trace.GotConn(ci)
}
