// Copyright 2025 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.

package http3

import (
	"context"
	"errors"
	"fmt"
	"math"
	"net/http"
	"net/url"
	"sync"

	"golang.org/x/net/quic"
)

// A transport is an HTTP/3 transport.
//
// It does not manage a pool of connections,
// and therefore does not implement net/http.RoundTripper.
//
// TODO: Provide a way to register an HTTP/3 transport with a net/http.transport's
// connection pool.
type transport struct {
	// config is the QUIC configuration used for client connections.
	config *quic.Config
	tr1    *http.Transport

	listenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)

	mu sync.Mutex // Guards fields below.
	// endpoint is the QUIC endpoint used by connections created by the
	// transport. If CloseIdleConnections is called when activeConns is empty,
	// endpoint will be unset. If unset, endpoint will be initialized by any
	// call to dial.
	endpoint      *quic.Endpoint
	activeConns   map[*clientConn]struct{}
	inFlightDials int
}

// netHTTPTransport implements the net/http.dialClientConner interface,
// allowing our HTTP/3 transport to integrate with net/http.
type netHTTPTransport struct {
	*transport
}

// RoundTrip is defined since Transport.RegisterProtocol takes in a
// RoundTripper. However, this method will never be used as net/http's
// dialClientConner interface does not have a RoundTrip method and will only
// use DialClientConn to create a new RoundTripper.
func (t netHTTPTransport) RoundTrip(*http.Request) (*http.Response, error) {
	panic("netHTTPTransport.RoundTrip should never be called")
}

func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, stateHook func()) (http.RoundTripper, error) {
	return t.transport.dial(ctx, addr, stateHook)
}

type TransportOpts struct {
	// ListenQUIC determines how the transport will open a QUIC endpoint.
	// By default, quic.Listen("udp", addr, config) is used.
	// ListenQUIC might be called multiple times.
	ListenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)

	// QUICConfig is the QUIC configuration used by the transport.
	// QUICConfig may be nil and should not be modified after calling
	// RegisterTransport.
	// If QUICConfig.TLSConfig is nil, the TLSConfig of the net/http Transport
	// given to RegisterTransport will be used.
	QUICConfig *quic.Config
}

// RegisterTransport configures a net/http HTTP/1 Transport to use HTTP/3.
func RegisterTransport(tr *http.Transport, opts TransportOpts) {
	if opts.QUICConfig == nil {
		opts.QUICConfig = &quic.Config{}
	}
	if opts.QUICConfig.TLSConfig == nil {
		opts.QUICConfig.TLSConfig = tr.TLSClientConfig
	}
	if opts.ListenQUIC == nil {
		opts.ListenQUIC = func(addr string, config *quic.Config) (*quic.Endpoint, error) {
			return quic.Listen("udp", addr, config)
		}
	}
	tr3 := &transport{
		// initConfig will clone the tr.TLSClientConfig.
		config:      initConfig(opts.QUICConfig),
		tr1:         tr,
		listenQUIC:  opts.ListenQUIC,
		activeConns: make(map[*clientConn]struct{}),
	}
	tr.RegisterProtocol("http/3", netHTTPTransport{tr3})
}

func (tr *transport) incInFlightDials() {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	tr.inFlightDials++
}

func (tr *transport) decInFlightDials() {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	tr.inFlightDials--
}

func (tr *transport) initEndpoint() (err error) {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	// This might cause rare issues on Darwin. Unlike Linux, Darwin kernel
	// seems to have the following behaviors:
	// - After closing a UDP socket, the port that was bound to the socket
	//   might not be immediately usable again.
	// - When doing IPv6 dual-stack binding (e.g., bind to ":0"), it will
	//   happily bind the IPv6 port, even when the IPv4 port is unavailable.
	//
	// When both of these are combined, in practice, it is possible for the
	// following to happen:
	// 1. Transport binds ":0", creating a dual-stack IPv6 UDP socket.
	//    Everything works as expected.
	// 2. At some point, CloseIdleConnections is called and the socket is
	//    closed.
	// 3. Soon after, a new dial is started, and a new dual-stack IPv6 socket
	//    is coincidentally assigned the same port as the previous socket.
	// 4. If the IPv4 port is still unavailable, Darwin's permissive binding
	//    behavior will cause us to have a socket that silently is unable to
	//    receive packets on its IPv4 address.
	// 5. If the dial target is an IPv4 address, transport will be able to send
	//    packets to the target, but will be unable to receive its reply.
	//
	// TransportOpts.ListenQUIC can technically be configured to avoid
	// dual-stack binding to avoid this issue, and high socket churn is
	// probably uncommon for regular use cases. However, finding a workaround
	// for this eventually would be ideal.
	if tr.endpoint == nil {
		tr.endpoint, err = tr.listenQUIC(":0", tr.config)
	}
	return err
}

// dial creates a new HTTP/3 client connection.
func (tr *transport) dial(ctx context.Context, target string, stateHook func()) (*clientConn, error) {
	tr.incInFlightDials()
	defer tr.decInFlightDials()

	if err := tr.initEndpoint(); err != nil {
		return nil, err
	}
	qconn, err := tr.endpoint.Dial(ctx, "udp", target, tr.config)
	if err != nil {
		return nil, err
	}
	return tr.newClientConn(ctx, qconn, stateHook)
}

// CloseIdleConnections is called by net/http.Transport.CloseIdleConnections
// after all existing idle connections are closed using http3.clientConn.Close.
//
// When the transport has no active connections anymore, calling this method
// will make the transport clean up any shared resources that are no longer
// required, such as its QUIC endpoint.
func (tr *transport) CloseIdleConnections() {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	if tr.endpoint == nil || len(tr.activeConns) > 0 || tr.inFlightDials > 0 {
		return
	}
	tr.endpoint.Close(canceledCtx)
	tr.endpoint = nil
}

// A clientConn is a client HTTP/3 connection.
//
// Multiple goroutines may invoke methods on a clientConn simultaneously.
type clientConn struct {
	tr *transport

	qconn *quic.Conn
	genericConn

	enc qpackEncoder
	dec qpackDecoder

	// Guarded by genericConn.mu
	reserved int
	active   int
	closed   bool

	stateHook func()
}

func (tr *transport) registerConn(cc *clientConn) {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	tr.activeConns[cc] = struct{}{}
}

func (tr *transport) unregisterConn(cc *clientConn) {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	delete(tr.activeConns, cc)
}

func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn, stateHook func()) (*clientConn, error) {
	cc := &clientConn{
		tr:        tr,
		qconn:     qconn,
		stateHook: stateHook,
	}
	tr.registerConn(cc)
	cc.enc.init()

	// Create control stream and send SETTINGS frame.
	controlStream, err := newConnStream(ctx, cc.qconn, streamTypeControl)
	if err != nil {
		tr.unregisterConn(cc)
		return nil, fmt.Errorf("http3: cannot create control stream: %v", err)
	}
	controlStream.writeSettings()
	controlStream.Flush()

	go func() {
		cc.acceptStreams(qconn, cc)
		cc.mu.Lock()
		cc.closed = true
		cc.mu.Unlock()
		tr.unregisterConn(cc)
		cc.maybeCallStateHook()
	}()
	return cc, nil
}

func (cc *clientConn) Close() error {
	// We need to use Close rather than Abort on the QUIC connection.
	// Otherwise, when a net/http.Transport.CloseIdleConnections is called, it
	// might call the http3.transport.CloseIdleConnections prior to all idle
	// connections being fully closed; this would make it unable to close its
	// QUIC endpoint, making http3.transport.CloseIdleConnections a no-op
	// unintentionally.
	return cc.qconn.Close()
}

func (cc *clientConn) Err() error {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	if cc.closed {
		return errors.New("connection closed")
	}
	return nil
}

func (cc *clientConn) Reserve() error {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	if cc.closed {
		return errors.New("connection closed")
	}
	cc.reserved++
	return nil
}

func (cc *clientConn) Release() {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	// This is consistent with RoundTrip: both Release and RoundTrip will
	// consume a reservation iff one exists.
	if cc.reserved > 0 {
		cc.reserved--
	}
}

func (cc *clientConn) Available() int {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	if cc.closed {
		return 0
	}
	// The general recommendation for HTTP/3 is to reuse the same connection
	// for multiple requests rather than creating new connections. As of now,
	// we don't have a good understanding of when one might want to create
	// multiple HTTP/3 connections to the same server.
	// Therefore, for ClientConn API, let HTTP/3 connections have no limit.
	// Starting a new RoundTrip when we are at the connection limit will just
	// block until a new max stream limit is received.
	return math.MaxInt
}

func (cc *clientConn) InFlight() int {
	cc.mu.Lock()
	defer cc.mu.Unlock()
	if cc.closed {
		return 0
	}
	return cc.reserved + cc.active
}

func (cc *clientConn) maybeCallStateHook() {
	if cc.stateHook != nil {
		cc.stateHook()
	}
}

func (cc *clientConn) handleControlStream(st *stream) error {
	// "A SETTINGS frame MUST be sent as the first frame of each control stream [...]"
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2
	if err := st.readSettings(func(settingsType, settingsValue int64) error {
		switch settingsType {
		case settingsMaxFieldSectionSize:
			_ = settingsValue // TODO
		case settingsQPACKMaxTableCapacity:
			_ = settingsValue // TODO
		case settingsQPACKBlockedStreams:
			_ = settingsValue // TODO
		default:
			// Unknown settings types are ignored.
		}
		return nil
	}); err != nil {
		return err
	}

	for {
		ftype, err := st.readFrameHeader()
		if err != nil {
			return err
		}
		switch ftype {
		case frameTypeCancelPush:
			// "If a CANCEL_PUSH frame is received that references a push ID
			// greater than currently allowed on the connection,
			// this MUST be treated as a connection error of type H3_ID_ERROR."
			// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-7
			return &connectionError{
				code:    errH3IDError,
				message: "CANCEL_PUSH received when no MAX_PUSH_ID has been sent",
			}
		case frameTypeGoaway:
			// TODO: Wait for requests to complete before closing connection.
			return errH3NoError
		default:
			// Unknown frames are ignored.
			if err := st.discardUnknownFrame(ftype); err != nil {
				return err
			}
		}
	}
}

func (cc *clientConn) handleEncoderStream(*stream) error {
	// TODO
	return nil
}

func (cc *clientConn) handleDecoderStream(*stream) error {
	// TODO
	return nil
}

func (cc *clientConn) handlePushStream(*stream) error {
	// "A client MUST treat receipt of a push stream as a connection error
	// of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent [...]"
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-4.6-3
	return &connectionError{
		code:    errH3IDError,
		message: "push stream created when no MAX_PUSH_ID has been sent",
	}
}

func (cc *clientConn) handleRequestStream(st *stream) error {
	// "Clients MUST treat receipt of a server-initiated bidirectional
	// stream as a connection error of type H3_STREAM_CREATION_ERROR [...]"
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3
	return &connectionError{
		code:    errH3StreamCreationError,
		message: "server created bidirectional stream",
	}
}

// abort closes the connection with an error.
func (cc *clientConn) abort(err error) {
	if e, ok := err.(*connectionError); ok {
		cc.qconn.Abort(&quic.ApplicationError{
			Code:   uint64(e.code),
			Reason: e.message,
		})
	} else {
		cc.qconn.Abort(err)
	}
}
